diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..c56eb39bb9e --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at office@openems.io. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations + diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000000..16b23745dbb --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing + +- [📜 Contributor License Agreement](#-contributor-license-agreement) +- [🤝 Contributor Code of Conduct](#-contributor-code-of-conduct) +- [🪲 Bug Reports, Issues and Questions](#-bug-reports-issues-and-questions) +- [💡 Feature Requests](#-feature-requests) +- [🖌️ Coding Style](#️-coding-style) +- [📁 Documentation](#-documentation) + +## 📜 Contributor License Agreement + +By contributing, you agree to the Licenses of this repository: + +- OpenEMS Edge und Backend + + [Eclipse Public License version 2.0](../LICENSE-EPL-2.0) + +- OpenEMS UI + + [GNU Affero General Public License version 3](../LICENSE-AGPL-3.0) + +## 🤝 Contributor Code of Conduct + +By contributing, you agree to respect the [Code of Conduct](CODE_OF_CONDUCT.md) of this repository. + +## 🪲 Bug Reports, Issues and Questions + +A great way to contribute to the project is to send a detailed report when you encounter an issue. We always appreciate a well-written, thorough bug report, and will thank you for it! + +To maintain clear and organized communication, all discussions should take place in the [OpenEMS Community forum](https://community.openems.io/). This helps keep everything together and ensures that conversations are streamlined and accessible to all contributors. + +⚠️ *Please refrain from opening empty pull requests solely for the purpose of discussing ideas. Instead, utilize the forum to share and refine your concepts before submitting any code changes.* + +## 💡 Feature Requests + +Similar to issue reports, feature requests should be submitted to the [OpenEMS Community forum](https://community.openems.io/) for discussion. This helps ensure that all ideas are properly reviewed and discussed by the community. + +When submitting a feature request, please be clear about the intended outcome and how it would relate to existing features. Providing detailed information helps in evaluating the request more effectively. + +⚠️ *Additionally, avoid submitting duplicate feature requests. Before posting, search for existing requests, and if you find a similar or identical one, please join that discussion instead of creating a new thread.* + +## 🖌️ Coding Style + +We welcome pull-requests! While we will consider all submissions, we cannot promise that every request will be accepted. + +To increase your changes of a merged pull-requests and help us review your code, please follow our [coding guidelines](https://openems.github.io/openems.io/openems/latest/contribute/coding-guidelines.html). + +## 📁 Documentation + +The documentation site for OpenEMS is hosted on [https://openems.github.io/openems.io/openems/latest](https://openems.github.io/openems.io/openems/latest). We greatly appreciate contributions that improve the quality and clarity of our documentation. + +If you would like to contribute by updating or adding a page, please refer to our [Contribute/Documentation](https://openems.github.io/openems.io/openems/latest/contribute/documentation.html) guide for detailed instructions on how to contribute effectively to the OpenEMS documentation. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index e81592e5d45..00000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,14 +0,0 @@ - -### Bug Report or Feature Request (mark with an `x`) -``` -- [ ] bug report -> please search issues before submitting -- [ ] feature request -``` - -### Bug description or desired functionality. - diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000000..27690e4ce33 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,38 @@ +name: Bug Report +description: Found something you weren't expecting? Report it here! +labels: ["type/bug"] +body: + - type: markdown + attributes: + value: | + 1. Please speak English, this is the language all maintainers can speak and write. + 2. Please ask questions or configuration/deploy problems on our [Community forum](https://community.openems.io/). + 3. Make sure you are using the latest release and + take a moment to check that your issue hasn't been reported before. + 4. It's really important to provide pertinent details and logs. + - type: textarea + id: description + attributes: + label: Description + description: | + Please provide a description of your issue here. + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Please provide one or more screenshots, if feasible. + - type: input + id: os-ver + attributes: + label: Operating System + description: The operating system you are running on. + - type: textarea + id: reproduce-info + attributes: + label: How to reproduce the Error? + description: | + Please provide step-by-step instructions on how to reproduce the error. + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..fe2aaeb311e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: OpenEMS.io + url: https://openems.io/ + about: News and general information are posted here. + - name: OpenEMS Community + url: https://community.openems.io/ + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 00000000000..c27dc9306d0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,29 @@ +name: Feature Request +description: Got an idea for a feature that OpenEMS is still missing? Submit your idea here! +labels: ["type/enhancement"] +body: + - type: markdown + attributes: + value: | + 1. Please speak English, this is the language all maintainers can speak and write. + 2. Please ask questions or configuration/deploy problems on our [Community forum](https://community.openems.io/). + 3. Please take a moment to check that your feature hasn't already been suggested. + - type: dropdown + id: component + attributes: + label: Component + description: Select the relevant component for this feature request. + options: + - "Edge" + - "UI" + - "Backend" + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + placeholder: | + What is the use case? + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/hacktober.yml b/.github/ISSUE_TEMPLATE/hacktober.yml new file mode 100644 index 00000000000..5a0bb4e6a4b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/hacktober.yml @@ -0,0 +1,70 @@ +name: Hacktoberfest +description: Perfect contribution for Hacktoberfest +labels: ["hacktoberfest"] +body: + - type: markdown + attributes: + value: | + 1. Please speak English, this is the language all maintainers can speak and write. + - type: dropdown + id: type + attributes: + label: Contribution Type + description: Select the relevant Contribution type. + options: + - "Feature" + - "Bug Fix" + - "Documentation" + validations: + required: true + - type: dropdown + id: component + attributes: + label: Component + description: Select the relevant component for this feature request. + options: + - "Edge" + - "UI" + - "Backend" + validations: + required: true + - type: dropdown + id: contributor-level + attributes: + label: Contributor Level + description: What OpenEMS knowledge level is required for the task? + options: + - "Newbie" + - "Casual" + - "Expert" + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + placeholder: | + What purpose does the task serve? + validations: + required: true + - type: textarea + id: definition-of-done + attributes: + label: Definition of Done + placeholder: | + What result is expected? + validations: + required: true + - type: textarea + id: difficulties + attributes: + label: Difficulties + placeholder: | + What difficulties may occur? + - type: markdown + attributes: + value: | + --- + - *For questions and suggestions please interact with the [community](https://community.openems.io/).* + - *Do you need help getting started? Then please read the documentation [documentation](https://openems.github.io/openems.io/openems/latest/introduction.html).* + - *Also read our [contribution guidelines](https://github.com/OpenEMS/openems/blob/develop/.github/CONTRIBUTING.md) before submitting code suggestions.* \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a7dddbfd65c..fe2414bdf30 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,6 +10,10 @@ updates: influxdb: patterns: - "com.influxdb:*" + fastexcel: + patterns: + - "org.dhatim:fastexcel" + - "org.dhatim:fastexcel-reader" - package-ecosystem: npm directory: "/ui" @@ -21,6 +25,8 @@ updates: angular: patterns: - "@angular/*" + - "@angular-devkit/*" + - "@angular-eslint/*" capacitor: patterns: - "@capacitor/*" @@ -36,7 +42,6 @@ updates: - "karma" eslint: patterns: - - "@angular-eslint/*" - "@stylistic/eslint-plugin" - "@typescript-eslint/*" - "eslint-*" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6f1d4022ad0..b03b5287e16 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,6 +43,7 @@ jobs: - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4 with: + flags: java token: ${{ secrets.CODECOV_TOKEN }} build-ui: @@ -77,5 +78,6 @@ jobs: - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4 with: + flags: ui directory: ./ui/ token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gradle-wrapper/gradle-wrapper.jar b/.gradle-wrapper/gradle-wrapper.jar index 2c3521197d7..a4b76b9530d 100644 Binary files a/.gradle-wrapper/gradle-wrapper.jar and b/.gradle-wrapper/gradle-wrapper.jar differ diff --git a/.gradle-wrapper/gradle-wrapper.properties b/.gradle-wrapper/gradle-wrapper.properties index 9355b415575..0aaefbcaf0f 100644 --- a/.gradle-wrapper/gradle-wrapper.properties +++ b/.gradle-wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/README.md b/README.md index 09bf75eeaac..519047007e3 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ OpenEMS is generally used in combination with external hardware and software com * Open up a [Live-Demo on Gitpod](https://gitpod.io/#https://github.com/OpenEMS/openems) * Follow the [Getting Started](https://openems.github.io/openems.io/openems/latest/gettingstarted.html) guide to setup OpenEMS on your own computer +* Please checkout our [contribution guidelines](CONTRIBUTING.md) before submitting code ## Documentation diff --git a/cnf/build.bnd b/cnf/build.bnd index 3a846baa49b..00cba399ba6 100644 --- a/cnf/build.bnd +++ b/cnf/build.bnd @@ -40,7 +40,7 @@ buildpath: \ org.osgi.service.metatype;version='1.4.1',\ org.osgi.service.metatype.annotations;version='1.4.1',\ org.osgi.util.promise;version='1.2.0',\ - com.google.guava;version='33.3.0.jre',\ + com.google.guava;version='33.3.1.jre',\ com.google.gson;version='2.11.0',\ testpath: \ diff --git a/cnf/pom.xml b/cnf/pom.xml index fe40c659af6..83b8c69ddd9 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -38,7 +38,7 @@ com.google.guava guava - 33.3.0-jre + 33.3.1-jre com.google.guava @@ -61,7 +61,7 @@ com.squareup.okio okio-jvm - 3.9.0 + 3.9.1 @@ -97,7 +97,7 @@ com.zaxxer HikariCP - 5.1.0 + 6.0.0 @@ -162,7 +162,7 @@ net.java.dev.jna jna - 5.14.0 + 5.15.0 @@ -203,7 +203,7 @@ org.apache.felix org.apache.felix.http.jetty - 5.1.24 + 5.1.26 @@ -237,7 +237,7 @@ org.apache.felix org.apache.felix.webconsole - 5.0.6 + 5.0.8 @@ -260,12 +260,12 @@ org.dhatim fastexcel - 0.18.1 + 0.18.4 org.dhatim fastexcel-reader - 0.18.1 + 0.18.4 @@ -306,7 +306,7 @@ org.jetbrains.kotlinx kotlinx-coroutines-core-jvm - 1.8.1 + 1.9.0 diff --git a/codecov.yml b/codecov.yml index 00b2350eb74..49e61532bad 100644 --- a/codecov.yml +++ b/codecov.yml @@ -14,3 +14,32 @@ comment: require_base: false require_head: true hide_project_coverage: true + +component_management: + default_rules: + statuses: + - type: project + target: auto + branches: + - "!main" + individual_components: + - component_id: openems_backend + name: "OpenEMS Backend" + paths: + - io.openems.backend.*/** + - io.openems.common/** + - io.openems.oem.*/** + - io.openems.shared.*/** + - io.openems.wrapper/** + - component_id: openems_edge + name: "OpenEMS Edge" + paths: + - io.openems.edge.*/** + - io.openems.common/** + - io.openems.oem.*/** + - io.openems.shared.*/** + - io.openems.wrapper/** + - component_id: openems_ui + name: "OpenEMS UI" + paths: + - ui/** \ No newline at end of file diff --git a/doc/modules/ROOT/pages/gettingstarted.adoc b/doc/modules/ROOT/pages/gettingstarted.adoc index 345571185fe..6ecd5effb4d 100644 --- a/doc/modules/ROOT/pages/gettingstarted.adoc +++ b/doc/modules/ROOT/pages/gettingstarted.adoc @@ -236,7 +236,7 @@ image::openems-ui-login.png[OpenEMS UI Login screen] .OpenEMS UI Energymonitor screen image::openems-ui-edge-overview.png[OpenEMS UI Energymonitor screen] -_Unfortunately the hosted version of OpenEMS UI is currently slightly outdated and incompatble with latest OpenEMS Edge. Follow the xref:ui/setup-ide.adoc[OpenEMS UI guide] to produce the following visualization. The language can be changed in the "burger menu" on top left -> btn:[admin] -> btn:[Allgemeine Einstellungen]._ +_Unfortunately the hosted version of OpenEMS UI is currently slightly outdated and incompatible with latest OpenEMS Edge. Follow the xref:ui/setup-ide.adoc[OpenEMS UI guide] to produce the following visualization. The language can be changed in the "burger menu" on top left -> btn:[admin] -> btn:[Allgemeine Einstellungen]._ .OpenEMS UI Energymonitor screen image::openems-ui-edge-overview2.png[OpenEMS UI Energymonitor screen] diff --git a/doc/modules/ROOT/pages/intellij.adoc b/doc/modules/ROOT/pages/intellij.adoc index de14e909977..e1ff0546267 100644 --- a/doc/modules/ROOT/pages/intellij.adoc +++ b/doc/modules/ROOT/pages/intellij.adoc @@ -36,7 +36,7 @@ image::intellij-import-bnd.png[] + .build.gradle image::intellij-build-gradle.png[] -.. or alternativly make an configuration "edit configuration" +.. or alternatively make an configuration "edit configuration" + .build gradle over configuration image::intellij-add-configuration.png[] diff --git a/io.openems.backend.application/BackendApp.bndrun b/io.openems.backend.application/BackendApp.bndrun index 923ac4c24c1..e210938f4fd 100644 --- a/io.openems.backend.application/BackendApp.bndrun +++ b/io.openems.backend.application/BackendApp.bndrun @@ -62,10 +62,10 @@ Java-WebSocket;version='[1.5.4,1.5.5)',\ com.fasterxml.aalto-xml;version='[1.3.3,1.3.4)',\ com.google.gson;version='[2.11.0,2.11.1)',\ - com.google.guava;version='[33.3.0,33.3.1)',\ + com.google.guava;version='[33.3.1,33.3.2)',\ com.google.guava.failureaccess;version='[1.0.2,1.0.3)',\ - com.squareup.okio;version='[3.9.0,3.9.1)',\ - com.zaxxer.HikariCP;version='[5.1.0,5.1.1)',\ + com.squareup.okio;version='[3.9.1,3.9.2)',\ + com.zaxxer.HikariCP;version='[6.0.0,6.0.1)',\ io.openems.backend.alerting;version=snapshot,\ io.openems.backend.application;version=snapshot,\ io.openems.backend.b2brest;version=snapshot,\ @@ -101,19 +101,19 @@ io.openems.wrapper.retrofit-converter-scalars;version=snapshot,\ io.openems.wrapper.retrofit2;version=snapshot,\ io.reactivex.rxjava3.rxjava;version='[3.1.9,3.1.10)',\ - org.apache.commons.commons-codec;version='[1.17.0,1.17.1)',\ - org.apache.commons.commons-compress;version='[1.26.2,1.26.3)',\ + org.apache.commons.commons-codec;version='[1.17.1,1.17.2)',\ + org.apache.commons.commons-compress;version='[1.27.1,1.27.2)',\ org.apache.commons.commons-csv;version='[1.11.0,1.11.1)',\ org.apache.commons.commons-io;version='[2.16.1,2.16.2)',\ org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ org.apache.felix.eventadmin;version='[1.6.4,1.6.5)',\ org.apache.felix.fileinstall;version='[3.7.4,3.7.5)',\ - org.apache.felix.http.jetty;version='[5.1.24,5.1.25)',\ + org.apache.felix.http.jetty;version='[5.1.26,5.1.27)',\ org.apache.felix.http.servlet-api;version='[3.0.0,3.0.1)',\ org.apache.felix.inventory;version='[2.0.0,2.0.1)',\ org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ org.apache.felix.scr;version='[2.2.12,2.2.13)',\ - org.apache.felix.webconsole;version='[5.0.6,5.0.7)',\ + org.apache.felix.webconsole;version='[5.0.8,5.0.9)',\ org.apache.felix.webconsole.plugins.ds;version='[2.3.0,2.3.1)',\ org.jetbrains.kotlin.osgi-bundle;version='[2.0.20,2.0.21)',\ org.jsr-305;version='[3.0.2,3.0.3)',\ diff --git a/io.openems.backend.timedata.aggregatedinflux/readme.adoc b/io.openems.backend.timedata.aggregatedinflux/readme.adoc index 077f29c83f5..b1cd9297cf3 100644 --- a/io.openems.backend.timedata.aggregatedinflux/readme.adoc +++ b/io.openems.backend.timedata.aggregatedinflux/readme.adoc @@ -45,7 +45,7 @@ This list must be adopted to a concrete usecase. It strongly depends on * your strategy to select component-IDs. * the components you are using within OpenEMS. -If you detect some widgets within your OpenEMS-UI which hava empty values, +If you detect some widgets within your OpenEMS-UI which have empty values, it may have to do with an incorrect hardcoded list. ==== @@ -57,7 +57,7 @@ the following configuration may provide a good start setup: *Create database and set retention policy:* -Before starting the OpenEMS backend and after intially setting up the influx servers, +Before starting the OpenEMS backend and after initially setting up the influx servers, you need to create the databases `influx0` and `aggregated0` and some retention policies: diff --git a/io.openems.common/src/io/openems/common/OpenemsConstants.java b/io.openems.common/src/io/openems/common/OpenemsConstants.java index 69b49e779ee..9b7384aae62 100644 --- a/io.openems.common/src/io/openems/common/OpenemsConstants.java +++ b/io.openems.common/src/io/openems/common/OpenemsConstants.java @@ -22,7 +22,7 @@ public class OpenemsConstants { *

* This is the month of the release. */ - public static final short VERSION_MINOR = 9; + public static final short VERSION_MINOR = 10; /** * The patch version of OpenEMS. diff --git a/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java b/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java index 4b0aabf35b8..cd0a08830ca 100644 --- a/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java +++ b/io.openems.common/src/io/openems/common/oem/DummyOpenemsEdgeOem.java @@ -60,6 +60,8 @@ public SystemUpdateParams getSystemUpdateParams() { .put("App.FENECON.Home", "https://fenecon.de/fenecon-home-10/") // .put("App.FENECON.Home.20", "https://fenecon.de/fenecon-home-20-30/") // .put("App.FENECON.Home.30", "https://fenecon.de/fenecon-home-20-30/") // + .put("App.FENECON.Commercial.92", "https://fenecon.de/fenecon-commercial/") // + .put("App.FENECON.Industrial.L.ILK710", "https://fenecon.de/fenecon-industrial-l/") // .put("App.FENECON.Industrial.S.ISK010", "https://fenecon.de/fenecon-industrial-s/") // .put("App.FENECON.Industrial.S.ISK110", "https://fenecon.de/fenecon-industrial-s/") // .put("App.FENECON.Industrial.S.ISK011", "https://fenecon.de/fenecon-industrial-s/") // @@ -92,8 +94,17 @@ public SystemUpdateParams getSystemUpdateParams() { .put("App.LoadControl.ThresholdControl", "") // .put("App.Meter.Socomec", "") // .put("App.Meter.CarloGavazzi", "") // + .put("App.Meter.PqPlus", "") // .put("App.Meter.Janitza", "") // .put("App.Meter.Discovergy", "")// + .put("App.Meter.PhoenixContact", "")// + .put("App.OpenemsHardware.BeagleBoneBlack", "") // + .put("App.OpenemsHardware.Compulab", "") // + .put("App.OpenemsHardware.CM3", "") // + .put("App.OpenemsHardware.CM4", "") // + .put("App.OpenemsHardware.CM4Max", "") // + .put("App.OpenemsHardware.CM4S", "") // + .put("App.OpenemsHardware.CM4S.Gen2", "") // .put("App.PvInverter.Fronius", "") // .put("App.PvInverter.Kaco", "") // .put("App.PvInverter.Kostal", "") // @@ -105,6 +116,7 @@ public SystemUpdateParams getSystemUpdateParams() { .put("App.Ess.FixStateOfCharge", "") // .put("App.Ess.PowerPlantController", "") // .put("App.Ess.PrepareBatteryExtension", "") // + .put("App.Ess.Limiter14a", "") // .build(); // NOTE: this will certainly get refactored in future, but it's a good start to diff --git a/io.openems.common/src/io/openems/common/websocket/MyDraft6455.java b/io.openems.common/src/io/openems/common/websocket/MyDraft6455.java index a89e41b5d39..3f549c9cb49 100644 --- a/io.openems.common/src/io/openems/common/websocket/MyDraft6455.java +++ b/io.openems.common/src/io/openems/common/websocket/MyDraft6455.java @@ -920,8 +920,8 @@ public void processFrame(WebSocketImpl webSocketImpl, Framedata frame) throws In } else if (curop == Opcode.BINARY) { this.processFrameBinary(webSocketImpl, frame); } else { - this.log.error("non control or continious frame expected"); - throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "non control or continious frame expected"); + this.log.error("non control or continuous frame expected"); + throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "non control or continuous frame expected"); } } diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index c621ec4d944..11442b68bce 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -200,10 +200,10 @@ com.fazecast.jSerialComm;version='[2.10.4,2.10.5)',\ com.ghgande.j2mod;version='[3.2.1,3.2.2)',\ com.google.gson;version='[2.11.0,2.11.1)',\ - com.google.guava;version='[33.3.0,33.3.1)',\ + com.google.guava;version='[33.3.1,33.3.2)',\ com.google.guava.failureaccess;version='[1.0.2,1.0.3)',\ - com.squareup.okio;version='[3.9.0,3.9.1)',\ - com.sun.jna;version='[5.14.0,5.14.1)',\ + com.squareup.okio;version='[3.9.1,3.9.2)',\ + com.sun.jna;version='[5.15.0,5.15.1)',\ io.openems.common;version=snapshot,\ io.openems.edge.application;version=snapshot,\ io.openems.edge.battery.api;version=snapshot,\ @@ -396,20 +396,20 @@ io.reactivex.rxjava3.rxjava;version='[3.1.9,3.1.10)',\ javax.jmdns;version='[3.4.1,3.4.2)',\ javax.xml.soap-api;version='[1.4.0,1.4.1)',\ - org.apache.commons.commons-codec;version='[1.17.0,1.17.1)',\ - org.apache.commons.commons-compress;version='[1.26.2,1.26.3)',\ + org.apache.commons.commons-codec;version='[1.17.1,1.17.2)',\ + org.apache.commons.commons-compress;version='[1.27.1,1.27.2)',\ org.apache.commons.commons-csv;version='[1.11.0,1.11.1)',\ org.apache.commons.commons-io;version='[2.16.1,2.16.2)',\ org.apache.commons.math3;version='[3.6.1,3.6.2)',\ org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ org.apache.felix.eventadmin;version='[1.6.4,1.6.5)',\ org.apache.felix.fileinstall;version='[3.7.4,3.7.5)',\ - org.apache.felix.http.jetty;version='[5.1.24,5.1.25)',\ + org.apache.felix.http.jetty;version='[5.1.26,5.1.27)',\ org.apache.felix.http.servlet-api;version='[3.0.0,3.0.1)',\ org.apache.felix.inventory;version='[2.0.0,2.0.1)',\ org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ org.apache.felix.scr;version='[2.2.12,2.2.13)',\ - org.apache.felix.webconsole;version='[5.0.6,5.0.7)',\ + org.apache.felix.webconsole;version='[5.0.8,5.0.9)',\ org.apache.felix.webconsole.plugins.ds;version='[2.3.0,2.3.1)',\ org.eclipse.jetty.client;version='[9.4.28,9.4.29)',\ org.eclipse.jetty.http;version='[9.4.28,9.4.29)',\ diff --git a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImpl.java b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImpl.java index c941c32eab6..efa68f7d43d 100644 --- a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImpl.java +++ b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImpl.java @@ -369,7 +369,7 @@ private void detectHardwareType() throws OpenemsException { /** * Get GoodWe hardware version from register value. * - * @param value Register value not formated with SCALE_FACTOR_MINUS_1 + * @param value Register value not formatted with SCALE_FACTOR_MINUS_1 * @return type as {@link GoodweHardwareType} or null */ public static BatteryFeneconHomeHardwareType parseHardwareTypeFromRegisterValue(int value) { diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/MulticastListener.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/MulticastListener.java index 73965b9d85c..1b509c0c834 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/MulticastListener.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/MulticastListener.java @@ -34,7 +34,7 @@ import java.net.UnknownHostException; /** - * Generic Mulitcast broadcast listener. Listens for a specific message and, in + * Generic Multicast broadcast listener. Listens for a specific message and, in * response, gives the specified reply. Used by NetAdapterHost for automatic * discovery of host components for the network-based DSPortAdapter. * diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/RawSendPacket.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/RawSendPacket.java index 538990d1130..38877239788 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/RawSendPacket.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/RawSendPacket.java @@ -57,7 +57,7 @@ class RawSendPacket { // -------- /** - * Construct and initiailize the raw send packet + * Construct and initialize the raw send packet */ public RawSendPacket() { this.buffer = new StringBuilder(); diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/UAdapterState.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/UAdapterState.java index 4aa06613e03..ed93664f6c5 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/UAdapterState.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/UAdapterState.java @@ -148,7 +148,7 @@ class UAdapterState { /** * This is the current 'real' speed that the OneWire is operating at. This is - * used to represent the actual mode that the DS2480 is operting in. For example + * used to represent the actual mode that the DS2480 is operating in. For example * the logical speed might be USPEED_REGULAR but for RF emission reasons we may * put the actual DS2480 in SPEED_FLEX. *

diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/USerialAdapter.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/USerialAdapter.java index 86062ea2a51..fae5e1ee211 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/USerialAdapter.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/adapter/USerialAdapter.java @@ -2305,7 +2305,7 @@ private boolean uAdapterPresent() throws OneWireException { /** * Do a master reset on the DS2480. This reduces the baud rate to 9600 and - * peforms a break. A single timing byte is then sent. + * performs a break. A single timing byte is then sent. */ private void uMasterReset() { if (doDebugMessages) { diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFile.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFile.java index 2ecb30f573d..577852c3d06 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFile.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFile.java @@ -145,7 +145,7 @@ *

  • File/directory names are not case sensitive and will be automatically * changed to all-CAPS *
  • Only files can have extensions - *
  • Extensions are numberical in the range 0 to 125 + *
  • Extensions are numerical in the range 0 to 125 *
  • Extensions 100 to 125 are special purpose and not yet implemented or * allowed *
  • Files can have the read-only attribute diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileInputStream.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileInputStream.java index cd5371b2015..4299e90101d 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileInputStream.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileInputStream.java @@ -51,7 +51,7 @@ *
  • File/directory names are not case sensitive and will be automatically * changed to all-CAPS *
  • Only files can have extensions - *
  • Extensions are numberical in the range 0 to 125 + *
  • Extensions are numerical in the range 0 to 125 *
  • Extensions 100 to 125 are special purpose and not yet implemented or * allowed *
  • Files can have the read-only attribute diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileOutputStream.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileOutputStream.java index 3f0335b23fa..18ed1be8b88 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileOutputStream.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileOutputStream.java @@ -70,7 +70,7 @@ *
  • File/directory names are not case sensitive and will be automatically * changed to all-CAPS *
  • Only files can have extensions - *
  • Extensions are numberical in the range 0 to 125 + *
  • Extensions are numerical in the range 0 to 125 *
  • Extensions 100 to 125 are special purpose and not yet implemented or * allowed *
  • Files can have the read-only attribute diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBankEEPROMstatus.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBankEEPROMstatus.java index f015e79cd1f..19d1cea33b7 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBankEEPROMstatus.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBankEEPROMstatus.java @@ -704,7 +704,7 @@ public boolean writeScratchpad(int addr, byte[] out_buf, int offset, int len) } /** - * Copy all 8 bytes of the Sratch Pad to a certain address in memory. + * Copy all 8 bytes of the Scratch Pad to a certain address in memory. * * @param addr the address to copy the data to * @param auth byte[] containing write authorization diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBankScratchSHAEE.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBankScratchSHAEE.java index 512b7a2ba17..2b57f4167e7 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBankScratchSHAEE.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBankScratchSHAEE.java @@ -467,7 +467,7 @@ public void copyScratchpad(int addr, byte[] scratchpad, int scratchpadOffset, by } /** - * Copy all 8 bytes of the Sratch Pad to a certain address in memory using the + * Copy all 8 bytes of the Scratch Pad to a certain address in memory using the * provided authorization MAC * * @param addr the address to copy the data to diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer33.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer33.java index e21c41885f8..541303b50e3 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer33.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer33.java @@ -822,7 +822,7 @@ private void initmem() { this.memoryPages[0].pageCRC = true; this.memoryPages[0].checked = false; - // Set memory bank varialbes + // Set memory bank variables this.memoryPages[1] = new MemoryBankSHAEE(this, this.mbScratchpad); this.memoryPages[1].bankDescription = "Page One with EPROM mode and write protection."; this.memoryPages[1].generalPurposeMemory = true; @@ -836,7 +836,7 @@ private void initmem() { this.memoryPages[1].pageCRC = true; this.memoryPages[1].checked = false; - // Set memory bank varialbes + // Set memory bank variables this.memoryPages[2] = new MemoryBankSHAEE(this, this.mbScratchpad); this.memoryPages[2].bankDescription = "Page Two and Three with write protection."; this.memoryPages[2].generalPurposeMemory = true; @@ -1110,7 +1110,7 @@ public void readScratchpad(byte[] scratchpad, int offset, byte[] extraInfo) } /** - * Copy all 8 bytes of the Sratch Pad to a certain page and offset in memory. + * Copy all 8 bytes of the Scratch Pad to a certain page and offset in memory. * * @param targetPage the page to copy the data to * @param targetPageOffset the offset into the page to copy to @@ -1129,7 +1129,7 @@ public boolean copyScratchpad(int targetPage, int targetPageOffset, byte[] copy_ } /** - * Copy all 8 bytes of the Sratch Pad to a certain page and offset in memory. + * Copy all 8 bytes of the Scratch Pad to a certain page and offset in memory. * * The container secret must be set so that the container can produce the * correct MAC. diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer37.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer37.java index 693c0b021a0..26bbe4770ae 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer37.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer37.java @@ -870,7 +870,7 @@ public boolean verifyPassword(byte[] password, int offset, int type) throws OneW // ***************************************************************************** // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// Private initilizers +// Private initializers // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // ***************************************************************************** diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer41.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer41.java index 9267ff60c99..4c92bd471cd 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer41.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer41.java @@ -4454,7 +4454,7 @@ private void setDate(int timeReg, int year, int month, int day, byte[] state) { // ***************************************************************************** // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// Private initilizers +// Private initializers // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // ***************************************************************************** diff --git a/io.openems.edge.common/src/io/openems/edge/common/channel/Doc.java b/io.openems.edge.common/src/io/openems/edge/common/channel/Doc.java index 29257d4927c..8838ceff5fb 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/channel/Doc.java +++ b/io.openems.edge.common/src/io/openems/edge/common/channel/Doc.java @@ -5,6 +5,7 @@ import io.openems.common.channel.Level; import io.openems.common.channel.PersistencePriority; import io.openems.common.channel.Unit; +import io.openems.common.session.Language; import io.openems.common.types.OpenemsType; import io.openems.common.types.OptionsEnum; import io.openems.edge.common.channel.internal.AbstractDoc; @@ -135,6 +136,23 @@ public static StateChannelDoc of(Level level) { */ public String getText(); + /** + * Gets the translated text. Defaults to empty String. + * + * @param lang language to get translated text + * @return the text + */ + public String getText(Language lang); + + /** + * Sets the translation key. + * + * @param channelKey the translationKey of the channel + * @param clazz the class of the channel parent + * @return myself + */ + public Doc translationKey(Class clazz, String channelKey); + /** * Is the more verbose debug mode activated?. * @@ -153,5 +171,4 @@ public static StateChannelDoc of(Level level) { */ public > C createChannelInstance(OpenemsComponent component, io.openems.edge.common.channel.ChannelId channelId); - } diff --git a/io.openems.edge.common/src/io/openems/edge/common/channel/internal/AbstractDoc.java b/io.openems.edge.common/src/io/openems/edge/common/channel/internal/AbstractDoc.java index 1fe4388ba83..22c752983d4 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/channel/internal/AbstractDoc.java +++ b/io.openems.edge.common/src/io/openems/edge/common/channel/internal/AbstractDoc.java @@ -1,9 +1,12 @@ package io.openems.edge.common.channel.internal; import java.util.List; +import java.util.MissingResourceException; +import java.util.ResourceBundle; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Function; import io.openems.common.channel.AccessMode; import io.openems.common.channel.PersistencePriority; @@ -11,6 +14,7 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.function.ThrowingBiConsumer; import io.openems.common.function.ThrowingConsumer; +import io.openems.common.session.Language; import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.Channel; import io.openems.edge.common.channel.ChannelId; @@ -26,6 +30,8 @@ public abstract class AbstractDoc implements Doc { private final OpenemsType type; + private Function getTextFunction; + protected AbstractDoc(OpenemsType type) { this.type = type; } @@ -118,20 +124,56 @@ public T getInitialValue() { return this.initialValue; } - /* - * Description - */ - private String text = ""; - @Override public AbstractDoc text(String text) { - this.text = text; + this.getTextFunction = lang -> { + return text; + }; return this.self(); } @Override public String getText() { - return this.text; + return this.getText(Language.DEFAULT); + } + + @Override + public String getText(Language lang) { + if (this.getTextFunction == null) { + return ""; + } + return this.getTextFunction.apply(lang); + } + + @Override + public AbstractDoc translationKey(Class clazz, String channelKey) { + this.getTextFunction = lang -> { + var bundle = AbstractDoc.getResourceBundle(lang, clazz); + if (bundle != null && bundle.containsKey(channelKey)) { + var textTranslated = bundle.getString(channelKey); + return textTranslated; + } + if (lang != Language.EN) { + // TODO: Use Language.DEFAULT for default language + bundle = AbstractDoc.getResourceBundle(Language.EN, clazz); + if (bundle != null && bundle.containsKey(channelKey)) { + var textTranslated = bundle.getString(channelKey); + return textTranslated; + } + } + + return channelKey; + }; + return this; + } + + private static ResourceBundle getResourceBundle(Language lang, Class clazz) { + try { + return ResourceBundle.getBundle(clazz.getPackageName() + ".translation", lang.getLocal(), + clazz.getModule()); + } catch (MissingResourceException e) { + return null; + } } @Override diff --git a/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentContext.java b/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentContext.java index 7779c033683..71007da35d3 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentContext.java +++ b/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentContext.java @@ -33,9 +33,16 @@ public static DummyComponentContext from(AbstractComponentConfig configuration) } private final Dictionary properties; + private final ComponentInstance instance; - public DummyComponentContext(Dictionary properties) { + public DummyComponentContext(Dictionary properties, ComponentInstance instance) { + super(); this.properties = properties; + this.instance = instance; + } + + public DummyComponentContext(Dictionary properties) { + this(properties, null); } public DummyComponentContext() { @@ -85,8 +92,9 @@ public Bundle getUsingBundle() { } @Override + @SuppressWarnings("unchecked") public ComponentInstance getComponentInstance() { - return null; + return (ComponentInstance) this.instance; } @Override diff --git a/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentInstance.java b/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentInstance.java new file mode 100644 index 00000000000..16283c87c4e --- /dev/null +++ b/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentInstance.java @@ -0,0 +1,59 @@ +package io.openems.edge.common.test; + +import org.osgi.service.component.ComponentInstance; + +import io.openems.common.utils.FunctionUtils; + +public class DummyComponentInstance implements ComponentInstance { + + public static class DummyComponentInstanceBuilder { + + private Runnable dispose; + private S instance; + + public DummyComponentInstanceBuilder setDispose(Runnable dispose) { + this.dispose = dispose; + return this; + } + + public DummyComponentInstanceBuilder setInstance(S instance) { + this.instance = instance; + return this; + } + + public DummyComponentInstance build() { + return new DummyComponentInstance<>(this.dispose, this.instance); + } + + } + + /** + * Creates a builder for a {@link DummyComponentInstance}. + * + * @param the type of the service + * @return the builder + */ + public static DummyComponentInstanceBuilder create() { + return new DummyComponentInstanceBuilder<>(); + } + + private final Runnable dispose; + private final S instance; + + public DummyComponentInstance(Runnable dispose, S instance) { + super(); + this.dispose = dispose != null ? dispose : FunctionUtils::doNothing; + this.instance = instance; + } + + @Override + public void dispose() { + this.dispose.run(); + } + + @Override + public S getInstance() { + return this.instance; + } + +} \ No newline at end of file diff --git a/io.openems.edge.common/test/io/openems/edge/common/channel/ChannelTranslationTest.java b/io.openems.edge.common/test/io/openems/edge/common/channel/ChannelTranslationTest.java new file mode 100644 index 00000000000..b4bc56abff8 --- /dev/null +++ b/io.openems.edge.common/test/io/openems/edge/common/channel/ChannelTranslationTest.java @@ -0,0 +1,51 @@ +package io.openems.edge.common.channel; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import io.openems.common.channel.Level; +import io.openems.common.session.Language; + +public class ChannelTranslationTest { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + TEST_CHANNEL(Doc.of(Level.WARNING) // + .translationKey(ChannelTranslationTest.class, "Test.TestChannel")), // + ONLY_ENGLISH(Doc.of(Level.INFO) // + .translationKey(ChannelTranslationTest.class, "Test.OnlyEnglish")), // + NO_TRANSLATION(Doc.of(Level.OK) // + .text("No Translation")),; + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + + @Test + public void testTranslatedGermanChannelText() { + assertEquals("German Test", ChannelTranslationTest.ChannelId.TEST_CHANNEL.doc().getText(Language.DE)); + } + + @Test + public void testTranslatedEnglishChannelText() { + assertEquals("English Test", ChannelTranslationTest.ChannelId.TEST_CHANNEL.doc().getText(Language.EN)); + } + + @Test + public void testOnlyEnglishTranslationTest() { + assertEquals("Only English", ChannelTranslationTest.ChannelId.ONLY_ENGLISH.doc().getText(Language.DE)); + } + + @Test + public void testNoTranslation() { + assertEquals("No Translation", ChannelTranslationTest.ChannelId.NO_TRANSLATION.doc().getText(Language.DE)); + } +} diff --git a/io.openems.edge.common/test/io/openems/edge/common/channel/translation_de.properties b/io.openems.edge.common/test/io/openems/edge/common/channel/translation_de.properties new file mode 100644 index 00000000000..7762fa376ab --- /dev/null +++ b/io.openems.edge.common/test/io/openems/edge/common/channel/translation_de.properties @@ -0,0 +1 @@ +Test.TestChannel = German Test \ No newline at end of file diff --git a/io.openems.edge.common/test/io/openems/edge/common/channel/translation_en.properties b/io.openems.edge.common/test/io/openems/edge/common/channel/translation_en.properties new file mode 100644 index 00000000000..a0778bfbcd4 --- /dev/null +++ b/io.openems.edge.common/test/io/openems/edge/common/channel/translation_en.properties @@ -0,0 +1,2 @@ +Test.TestChannel = English Test +Test.OnlyEnglish = Only English \ No newline at end of file diff --git a/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/ApiWorker.java b/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/ApiWorker.java index 7ad767546d7..4e15cd6d476 100644 --- a/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/ApiWorker.java +++ b/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/ApiWorker.java @@ -10,6 +10,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,10 +21,12 @@ import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; import io.openems.common.jsonrpc.request.SetChannelValueRequest; import io.openems.common.types.OpenemsType; +import io.openems.common.utils.FunctionUtils; import io.openems.common.utils.JsonUtils; import io.openems.edge.common.channel.Channel; import io.openems.edge.common.channel.StringReadChannel; import io.openems.edge.common.channel.WriteChannel; +import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.user.User; @@ -40,7 +43,7 @@ public class ApiWorker { private final Logger log = LoggerFactory.getLogger(ApiWorker.class); - private final OpenemsComponent parent; + private final AbstractOpenemsComponent parent; /** * Debug information about writes to channels is sent to this channel. @@ -58,8 +61,24 @@ public class ApiWorker { private int timeoutSeconds = DEFAULT_TIMEOUT_SECONDS; - public ApiWorker(OpenemsComponent parent) { + /** + * Handles write-only channel overriding. + */ + public record WriteHandler(Consumer, WriteObject>> handleWrites, // + Consumer setOverrideStatus, // + Runnable handleTimeout) { + } + + private WriteHandler writeHandler; + + public ApiWorker(AbstractOpenemsComponent parent) { + this(parent, new WriteHandler(FunctionUtils::doNothing, // + FunctionUtils::doNothing, FunctionUtils::doNothing)); + } + + public ApiWorker(AbstractOpenemsComponent parent, WriteHandler writeHandler) { // this.parent = parent; + this.writeHandler = writeHandler; this.executor = Executors.newSingleThreadScheduledExecutor(); } @@ -150,6 +169,8 @@ private synchronized void resetTimeout() { + entry.getKey().address() + "] after [" + this.timeoutSeconds + "s]"); entry.getValue().notifyTimeout(); } + this.writeHandler.setOverrideStatus.accept(Status.INACTIVE); + this.writeHandler.handleTimeout.run(); this.values.clear(); } }, this.timeoutSeconds, TimeUnit.SECONDS); @@ -186,7 +207,11 @@ public void run() throws OpenemsNamedException { "Set Channel [" + channel.address() + "] to Value [" + writeObject.valueToString() + "]"); writeObject.setNextWriteValue(channel); writeObject.notifySuccess(); + logs.add(channel.address() + ":" + writeObject.valueToString()); + + this.writeHandler.handleWrites.accept(entry); + this.writeHandler.setOverrideStatus.accept(Status.ACTIVE); } catch (OpenemsException e) { OpenemsComponent.logError(this.parent, this.log, "Unable to set Channel [" + channel.address() + "] to Value [" + writeObject.valueToString() + "]: " + e.getMessage()); diff --git a/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/Status.java b/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/Status.java new file mode 100644 index 00000000000..14c18bde3e8 --- /dev/null +++ b/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/Status.java @@ -0,0 +1,37 @@ +package io.openems.edge.controller.api.common; + +import io.openems.common.types.OptionsEnum; + +public enum Status implements OptionsEnum { + + ACTIVE(0, "Active"), // + + INACTIVE(1, "Inactive"), // + + ERROR(2, "Error"); // + + + private final int value; + private final String name; + + private Status(int value, String name) { + this.value = value; + this.name = name; + } + + @Override + public int getValue() { + return this.value; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public OptionsEnum getUndefined() { + return INACTIVE; + } + +} diff --git a/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/WriteObject.java b/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/WriteObject.java index 25d3b9f8297..02c4533867d 100644 --- a/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/WriteObject.java +++ b/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/WriteObject.java @@ -118,6 +118,13 @@ public void notifyTimeout() { * @return the value as String */ public abstract String valueToString(); + + /** + * Gets the value of the current object. + * + * @return the value as the corresponding type + */ + public abstract Object value(); /** * Is there a defined value?. diff --git a/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/WritePojo.java b/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/WritePojo.java index c6d390c5eed..5a71dc9a94f 100644 --- a/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/WritePojo.java +++ b/io.openems.edge.controller.api.common/src/io/openems/edge/controller/api/common/WritePojo.java @@ -29,4 +29,9 @@ public boolean isNull() { return this.value == null; } + @Override + public Object value() { + return this.value; + } + } diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusTcpApi.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusTcpApi.java index d862b6c7177..52e5f67deae 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusTcpApi.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/AbstractModbusTcpApi.java @@ -1,8 +1,11 @@ package io.openems.edge.controller.api.modbus; +import java.util.Arrays; import java.util.List; +import java.util.Map.Entry; import java.util.TreeMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -35,6 +38,9 @@ import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable; import io.openems.edge.controller.api.Controller; import io.openems.edge.controller.api.common.ApiWorker; +import io.openems.edge.controller.api.common.ApiWorker.WriteHandler; +import io.openems.edge.controller.api.common.Status; +import io.openems.edge.controller.api.common.WriteObject; import io.openems.edge.controller.api.common.WritePojo; import io.openems.edge.controller.api.modbus.jsonrpc.GetModbusProtocolExportXlsxRequest; import io.openems.edge.controller.api.modbus.jsonrpc.GetModbusProtocolExportXlsxResponse; @@ -48,16 +54,15 @@ public abstract class AbstractModbusTcpApi extends AbstractOpenemsComponent public static final int DEFAULT_PORT = 502; public static final int DEFAULT_MAX_CONCURRENT_CONNECTIONS = 5; - protected final ApiWorker apiWorker = new ApiWorker(this); - - private final Logger log = LoggerFactory.getLogger(AbstractModbusTcpApi.class); - private final MyProcessImage processImage; - private final String implementationName; - /** * Holds the link between Modbus address and ModbusRecord. */ protected final TreeMap records = new TreeMap<>(); + protected final ApiWorker apiWorker = new ApiWorker(this, + new WriteHandler(this.handleWrites(), this::setOverrideStatus, this.handleTimeouts())); + private final Logger log = LoggerFactory.getLogger(AbstractModbusTcpApi.class); + private final MyProcessImage processImage; + private final String implementationName; /** * Holds the link between Modbus start address of a Component and the @@ -66,10 +71,12 @@ public abstract class AbstractModbusTcpApi extends AbstractOpenemsComponent private final TreeMap components = new TreeMap<>(); private ConfigRecord config; + private List invalidComponents = new CopyOnWriteArrayList<>(); protected synchronized void addComponent(OpenemsComponent component) { if (!(component instanceof ModbusSlave)) { this.logError(this.log, "Component [" + component.id() + "] does not implement ModbusSlave"); + this.invalidComponents.add(component); this._setComponentNoModbusApiFault(true); return; } @@ -77,8 +84,20 @@ protected synchronized void addComponent(OpenemsComponent component) { this.updateComponents(); } + protected abstract Consumer, WriteObject>> handleWrites(); + + protected abstract void setOverrideStatus(Status status); + + protected abstract Runnable handleTimeouts(); + protected synchronized void removeComponent(OpenemsComponent component) { - this._components.remove(component); + if (this.invalidComponents.remove(component)) { + if (this.invalidComponents.isEmpty()) { + this._setComponentNoModbusApiFault(false); + } + this._components.remove(component); + return; + } this.updateComponents(); } @@ -92,19 +111,39 @@ public AbstractModbusTcpApi(String implementationName, this.processImage = new MyProcessImage(this); } - protected void activate(ComponentContext context, String id, String alias, boolean enabled, ConfigurationAdmin cm, - ConfigRecord config) throws OpenemsException { - super.activate(context, id, alias, enabled); + protected void activate(ComponentContext context, ConfigurationAdmin cm, ConfigRecord config) + throws OpenemsException { + super.activate(context, config.id(), config.alias(), config.enabled()); + this.handleActivate(config, cm, config.id()); + } - // configuration settings - this.config = config; + protected void modified(ComponentContext context, ConfigurationAdmin cm, ConfigRecord config) + throws OpenemsException { + super.modified(context, config.id(), config.alias(), config.enabled()); // update filter for 'Components'; allow disable components final var filter = ConfigUtils.generateReferenceTargetFilter(this.servicePid(), false, config.componentIds); - if (OpenemsComponent.updateReferenceFilterRaw(cm, this.servicePid(), "Component", filter)) { + OpenemsComponent.updateReferenceFilterRaw(cm, this.servicePid(), "Component", filter); + + // Config (relevant for API) was not modified + if (this.config.equals(config)) { return; } + ModbusSlaveFactory.close(); + + // Activate with new config + this.handleModified(config, cm, config.id()); + } + + private void handleActivate(ConfigRecord config, ConfigurationAdmin cm, String id) { + // configuration settings + this.config = config; + + // update filter for 'Components'; allow disable components + final var filter = ConfigUtils.generateReferenceTargetFilter(this.servicePid(), false, config.componentIds); + OpenemsComponent.updateReferenceFilterRaw(cm, this.servicePid(), "Component", filter); + this.apiWorker.setTimeoutSeconds(config.apiTimeout); if (!this.isEnabled()) { @@ -118,6 +157,24 @@ protected void activate(ComponentContext context, String id, String alias, boole this.updateComponents(); } + private void handleModified(ConfigRecord config, ConfigurationAdmin cm, String id) { + // configuration settings + this.config = config; + + this.apiWorker.setTimeoutSeconds(config.apiTimeout); + + if (!this.isEnabled()) { + // abort if disabled + this.startApiWorker.deactivate(); + return; + } + + // Modify Modbus-Server + this.startApiWorker.modified(id); + + this.updateComponents(); + } + /** * Called by addComponent/removeComponent. Initializes the ModbusRecords, once * all Components are available. Fault-State otherwise. @@ -142,6 +199,7 @@ private synchronized void updateComponents() { @Override protected void deactivate() { + this.startApiWorker.deactivate(); ModbusSlaveFactory.close(); super.deactivate(); @@ -171,10 +229,11 @@ protected void forever() { this.slave = ModbusSlaveFactory.createTCPSlave(port, AbstractModbusTcpApi.this.config.maxConcurrentConnections); this.slave.addProcessImage(UNIT_ID, AbstractModbusTcpApi.this.processImage); - this.slave.open(); - AbstractModbusTcpApi.this.logInfo(this.log, AbstractModbusTcpApi.this.implementationName + if (isEnabled()) { + this.slave.open(); + AbstractModbusTcpApi.this.logInfo(this.log, AbstractModbusTcpApi.this.implementationName + " started on port [" + port + "] with UnitId [" + AbstractModbusTcpApi.UNIT_ID + "]."); - + } } catch (ModbusException e) { ModbusSlaveFactory.close(); AbstractModbusTcpApi.this.logError(this.log, @@ -431,21 +490,45 @@ protected ModbusSlave getPossiblyDisabledComponent(String componentId) { .orElse(null); } - protected static class ConfigRecord { - public final Meta metaComponent; - public final String[] componentIds; - public final int apiTimeout; - public final int port; - public final int maxConcurrentConnections; - - public ConfigRecord(Meta metaComponent, String[] componentIds, int apiTimeout, int port, - int maxConcurrentConnections) { - super(); - this.metaComponent = metaComponent; - this.componentIds = componentIds; - this.apiTimeout = apiTimeout; - this.port = port; - this.maxConcurrentConnections = maxConcurrentConnections; + public static record ConfigRecord(String id, String alias, boolean enabled, Meta metaComponent, + String[] componentIds, int apiTimeout, int port, int maxConcurrentConnections) { + + @Override + public boolean equals(Object other) { + + if (this == other) { + return true; + } + if (other == null) { + return false; + } + if (!(other instanceof ConfigRecord)) { + return false; + } + ConfigRecord config = (ConfigRecord) other; + + if (config.id.equals(this.id) && config.alias.equals(this.alias) // + && config.enabled == this.enabled && config.metaComponent.equals(this.metaComponent) // + && Arrays.equals(config.componentIds, this.componentIds) // + && config.apiTimeout == this.apiTimeout && config.port == this.port // + && config.maxConcurrentConnections == this.maxConcurrentConnections) { + return true; + } + + return false; + } } + + ; + + /** + * Format a given channelAddress to a ChannelId. + * + * @param channel WriteChannel + * @return component_channelId as String + */ + public static String formatChannelName(WriteChannel channel) { + return channel.getComponent().alias() + "_" + channel.channelId().name(); + } } diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/ModbusTcpApi.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/ModbusTcpApi.java index 21d232a7911..da838a2d947 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/ModbusTcpApi.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/ModbusTcpApi.java @@ -18,7 +18,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { .debounce(10, Debounce.TRUE_VALUES_IN_A_ROW_TO_SET_TRUE) // .text("A configured Component is not available")), // PROCESS_IMAGE_FAULT(Doc.of(Level.FAULT) // - .debounce(50, Debounce.FALSE_VALUES_IN_A_ROW_TO_SET_FALSE) // + .debounce(10, Debounce.FALSE_VALUES_IN_A_ROW_TO_SET_FALSE) // .text("Invalid Modbus Function call. Only FC3, FC4, FC6 and FC16 are supported")); private final Doc doc; diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnlyImpl.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnlyImpl.java index b983cbd8df4..01c6e47638a 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnlyImpl.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readonly/ControllerApiModbusTcpReadOnlyImpl.java @@ -1,5 +1,8 @@ package io.openems.edge.controller.api.modbus.readonly; +import java.util.Map.Entry; +import java.util.function.Consumer; + import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; @@ -16,10 +19,13 @@ import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; +import io.openems.edge.common.channel.WriteChannel; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.jsonapi.ComponentJsonApi; import io.openems.edge.common.meta.Meta; import io.openems.edge.controller.api.Controller; +import io.openems.edge.controller.api.common.Status; +import io.openems.edge.controller.api.common.WriteObject; import io.openems.edge.controller.api.modbus.AbstractModbusTcpApi; import io.openems.edge.controller.api.modbus.ModbusTcpApi; @@ -59,8 +65,8 @@ public ControllerApiModbusTcpReadOnlyImpl() { @Activate private void activate(ComponentContext context, Config config) throws ModbusException, OpenemsException { - super.activate(context, config.id(), config.alias(), config.enabled(), this.cm, - new ConfigRecord(this.metaComponent, config.component_ids(), 0 /* no timeout */, config.port(), + super.activate(context, this.cm, + new ConfigRecord(config.id(), config.alias(), config.enabled(),this.metaComponent, config.component_ids(), 0 /* no timeout */, config.port(), config.maxConcurrentConnections())); } @@ -74,4 +80,18 @@ protected void deactivate() { protected AccessMode getAccessMode() { return AccessMode.READ_ONLY; } + + @Override + protected Consumer, WriteObject>> handleWrites() { + return entry -> { }; + } + + @Override + protected void setOverrideStatus(Status status) { + } + + @Override + protected Runnable handleTimeouts() { + return () -> { }; + } } diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/Config.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/Config.java index 744d65b22bf..0614487de2f 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/Config.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/Config.java @@ -25,6 +25,13 @@ @AttributeDefinition(name = "Component-IDs", description = "Components that should be made available via Modbus.") String[] component_ids() default { "_sum" }; + // TODO: Currently unused + @AttributeDefinition(name = "Read Channel-IDs", description = "Contains the channelnames of all read channels.") + String[] readChannels() default { }; + + @AttributeDefinition(name = "Write Channel-IDs", description = "Contains the channelnames of all overridden channels.") + String[] writeChannels(); + @AttributeDefinition(name = "Api-Timeout", description = "Sets the timeout in seconds for updates on Channels set by this Api.") int apiTimeout() default 60; diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWrite.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWrite.java index 78c23fcc525..8a18a1aada2 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWrite.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWrite.java @@ -1,13 +1,33 @@ package io.openems.edge.controller.api.modbus.readwrite; +import static io.openems.common.channel.PersistencePriority.HIGH; +import static io.openems.common.channel.Unit.CUMULATED_SECONDS; +import static io.openems.common.types.OpenemsType.LONG; + +import io.openems.common.channel.PersistencePriority; import io.openems.common.types.OpenemsType; +import io.openems.edge.common.channel.Channel; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.channel.StringReadChannel; +import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.controller.api.common.Status; public interface ControllerApiModbusTcpReadWrite extends OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + + OVERRIDE_STATUS(Doc.of(Status.values()) // + .persistencePriority(PersistencePriority.HIGH)), // + + CUMULATED_ACTIVE_TIME(Doc.of(LONG)// + .unit(CUMULATED_SECONDS) // + .persistencePriority(HIGH)), // + + CUMULATED_INACTIVE_TIME(Doc.of(LONG)// + .unit(CUMULATED_SECONDS) // + .persistencePriority(HIGH)), // + API_WORKER_LOG(Doc.of(OpenemsType.STRING) // .text("Logs Write-Commands via ApiWorker")); // @@ -32,4 +52,31 @@ public default StringReadChannel getApiWorkerLogChannel() { return this.channel(ChannelId.API_WORKER_LOG); } + /** + * Gets the Channel for {@link ChannelId#OVERRIDE_STATUS}. + * + * @return the Channel + */ + public default Channel getOverrideStatusChannel() { + return this.channel(ChannelId.OVERRIDE_STATUS); + } + + /** + * Gets the Status. See {@link ChannelId#OVERRIDE_STATUS}. + * + * @return the Channel {@link Value} + */ + public default Status getOverrideStatus() { + return this.getOverrideStatusChannel().value().asEnum(); + } + + /** + * Internal method to set the 'nextValue' on {@link ChannelId#OVERRIDE_STATUS} Channel. + * + * @param value the next value + */ + public default void _setOverrideStatus(Status value) { + this.getOverrideStatusChannel().setNextValue(value); + } + } diff --git a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImpl.java b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImpl.java index af51ee0b092..74055d005b1 100644 --- a/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImpl.java +++ b/io.openems.edge.controller.api.modbus/src/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImpl.java @@ -1,27 +1,49 @@ package io.openems.edge.controller.api.modbus.readwrite; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map.Entry; +import java.util.function.Consumer; + +import org.osgi.service.cm.Configuration; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.ConfigurationPolicy; import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Modified; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.ghgande.j2mod.modbus.ModbusException; import io.openems.common.channel.AccessMode; +import io.openems.common.channel.PersistencePriority; +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.OpenemsType; +import io.openems.edge.common.channel.ChannelId.ChannelIdImpl; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.channel.WriteChannel; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.jsonapi.ComponentJsonApi; import io.openems.edge.common.meta.Meta; import io.openems.edge.controller.api.Controller; +import io.openems.edge.controller.api.common.Status; +import io.openems.edge.controller.api.common.WriteObject; import io.openems.edge.controller.api.modbus.AbstractModbusTcpApi; import io.openems.edge.controller.api.modbus.ModbusTcpApi; +import io.openems.edge.timedata.api.Timedata; +import io.openems.edge.timedata.api.TimedataProvider; +import io.openems.edge.timedata.api.utils.CalculateActiveTime; @Designate(ocd = Config.class, factory = true) @Component(// @@ -30,7 +52,22 @@ configurationPolicy = ConfigurationPolicy.REQUIRE // ) public class ControllerApiModbusTcpReadWriteImpl extends AbstractModbusTcpApi - implements ControllerApiModbusTcpReadWrite, ModbusTcpApi, Controller, OpenemsComponent, ComponentJsonApi { + implements ControllerApiModbusTcpReadWrite, ModbusTcpApi, Controller, OpenemsComponent, ComponentJsonApi, TimedataProvider { + + private final Logger log = LoggerFactory.getLogger(ControllerApiModbusTcpReadWriteImpl.class); + + private final CalculateActiveTime calculateCumulatedActiveTime = new CalculateActiveTime(this, + ControllerApiModbusTcpReadWrite.ChannelId.CUMULATED_ACTIVE_TIME); + + private final CalculateActiveTime calculateCumulatedInactiveTime = new CalculateActiveTime(this, + ControllerApiModbusTcpReadWrite.ChannelId.CUMULATED_INACTIVE_TIME); + + private List writeChannels; + + private boolean isActive = false; + + @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) + private volatile Timedata timedata = null; @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) private Meta metaComponent = null; @@ -39,7 +76,11 @@ public class ControllerApiModbusTcpReadWriteImpl extends AbstractModbusTcpApi private ConfigurationAdmin cm; @Override - @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MULTIPLE) + @Reference(// + policy = ReferencePolicy.DYNAMIC, // + policyOption = ReferencePolicyOption.GREEDY, // + cardinality = ReferenceCardinality.MULTIPLE // + ) protected void addComponent(OpenemsComponent component) { super.addComponent(component); } @@ -60,9 +101,24 @@ public ControllerApiModbusTcpReadWriteImpl() { @Activate private void activate(ComponentContext context, Config config) throws ModbusException, OpenemsException { - super.activate(context, config.id(), config.alias(), config.enabled(), this.cm, - new ConfigRecord(this.metaComponent, config.component_ids(), config.apiTimeout(), config.port(), - config.maxConcurrentConnections())); + super.activate(context, this.cm, + new ConfigRecord(config.id(), config.alias(), config.enabled(), this.metaComponent, + config.component_ids(), config.apiTimeout(), config.port(), config.maxConcurrentConnections())); + this.applyConfig(config); + this.handleTimeDataChannels(); + } + + @Modified + private void modified(ComponentContext context, Config config) throws OpenemsNamedException { + super.modified(context, this.cm, + new ConfigRecord(config.id(), config.alias(), config.enabled(), this.metaComponent, + config.component_ids(), config.apiTimeout(), config.port(), config.maxConcurrentConnections())); + this.applyConfig(config); + this.handleTimeDataChannels(); + } + + private void applyConfig(Config config) { + this.writeChannels = new ArrayList<>(Arrays.asList(config.writeChannels())); } @Override @@ -70,9 +126,102 @@ private void activate(ComponentContext context, Config config) throws ModbusExce protected void deactivate() { super.deactivate(); } + + @Override + public void run() throws OpenemsNamedException { + this.isActive = false; + super.run(); + + this.calculateCumulatedActiveTime.update(this.isActive); + this.calculateCumulatedInactiveTime.update(!this.isActive); + } @Override protected AccessMode getAccessMode() { return AccessMode.READ_WRITE; } + + /** + * Updating the configuration property to given value. + * + * @param targetProperty Property that should be changed + * @param requiredValue Value that should be set + */ + private void configUpdate(String targetProperty, String requiredValue) { + Configuration c; + try { + var pid = this.servicePid(); + if (pid.isEmpty()) { + this.logInfo(this.log, "PID of " + this.id() + " is Empty"); + return; + } + c = this.cm.getConfiguration(pid, "?"); + var properties = c.getProperties(); + if (!this.writeChannels.contains(requiredValue)) { + this.writeChannels.add(requiredValue); + properties.put(targetProperty, this.writeChannels.toArray(String[]::new)); + c.update(properties); + } + } catch (IOException | SecurityException e) { + this.logError(this.log, "ERROR: " + e.getMessage()); + } + } + + @Override + protected Consumer, WriteObject>> handleWrites() { + return entry -> { + this.isActive = true; + WriteChannel channel = entry.getKey(); + var writeObject = entry.getValue(); + + String channelName = formatChannelName(channel); + var currentChannel = new ChannelIdImpl(channelName, + Doc.of(channel.getType()).persistencePriority(PersistencePriority.HIGH)); + if (!channels().stream().anyMatch(p -> p.channelId().name().equals(currentChannel.name()))) { + addChannel(currentChannel).setNextValue(writeObject.value()); + } else { + channel(currentChannel).setNextValue(writeObject.value()); + } + this.configUpdate("writeChannels", channel(currentChannel).channelId().id()); + }; + } + + @Override + protected void setOverrideStatus(Status status) { + this._setOverrideStatus(status); + } + + @Override + protected Runnable handleTimeouts() { + return () -> { + this.writeChannels.forEach(c -> { + channels().stream().filter(channel -> channel.channelId().id().equals(c)).findFirst() + .ifPresent(channel -> channel.setNextValue(null)); + }); + }; + } + + @Override + public Timedata getTimedata() { + return this.timedata; + } + + /** + * Checks, if timedata channels are already set. + * If not, they will be created and added to current channels. + */ + protected void handleTimeDataChannels() { + var activeTimeChannel = new ChannelIdImpl("CUMULATED_ACTIVE_TIME", // + Doc.of(OpenemsType.DOUBLE).persistencePriority(PersistencePriority.HIGH)); + var inactiveTimeChannel = new ChannelIdImpl("CUMULATED_INACTIVE_TIME", // + Doc.of(OpenemsType.DOUBLE).persistencePriority(PersistencePriority.HIGH)); + + List timeChannels = Arrays.asList(activeTimeChannel, inactiveTimeChannel); + timeChannels.forEach(channel -> { + if (channels().stream().noneMatch(ch -> ch.channelId().id().equals(channel.id()))) { + addChannel(channel); + } + }); + } + } diff --git a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImplTest.java b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImplTest.java index 5da230c70a8..58ea9ca90c5 100644 --- a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImplTest.java +++ b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/ControllerApiModbusTcpReadWriteImplTest.java @@ -1,9 +1,11 @@ package io.openems.edge.controller.api.modbus.readwrite; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import org.junit.Test; - import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.common.test.DummyCycle; import io.openems.edge.controller.api.modbus.AbstractModbusTcpApi; import io.openems.edge.controller.test.ControllerTest; @@ -26,4 +28,21 @@ public void test() throws Exception { .next(new TestCase()) // ; } + + @Test + public void testTimedataChannels() throws Exception { + var controller = new ControllerApiModbusTcpReadWriteImpl(); // + boolean channelNotFound = controller.channels().stream().noneMatch(// + ch -> ch.channelId().id().equals("CumulatedActiveTime") // + || ch.channelId().id().equals("CumulatedInactiveTime")); // + assertFalse(channelNotFound); + } + + @Test + public void testAddFalseComponents() throws Exception { + var controller = new ControllerApiModbusTcpReadWriteImpl(); // + controller.addComponent(new DummyCycle(1000)); // + controller.getComponentNoModbusApiFaultChannel().nextProcessImage(); // + assertTrue(controller.getComponentNoModbusApiFault().get()); // + } } diff --git a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/MyConfig.java b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/MyConfig.java index 54489b1473a..5c06b078554 100644 --- a/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/MyConfig.java +++ b/io.openems.edge.controller.api.modbus/test/io/openems/edge/controller/api/modbus/readwrite/MyConfig.java @@ -1,7 +1,12 @@ package io.openems.edge.controller.api.modbus.readwrite; +import java.nio.channels.Channels; + import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.types.EdgeConfig.Component.Channel; import io.openems.common.utils.ConfigUtils; +import io.openems.edge.common.channel.ChannelId; +import io.openems.edge.common.channel.ChannelId.ChannelIdImpl; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { @@ -13,9 +18,21 @@ protected static class Builder { private String[] componentIds; private int maxConcurrentConnections; private int apiTimeout; + private String[] writeChannels = {}; + private String[] readChannels = {}; private Builder() { } + + public Builder setWriteChannels(String... writeChannels) { + this.writeChannels = writeChannels; + return this; + } + + public Builder setReadChannels(String... readChannels) { + this.readChannels = readChannels; + return this; + } public Builder setId(String id) { this.id = id; @@ -98,4 +115,14 @@ public int apiTimeout() { return this.builder.apiTimeout; } + @Override + public String[] readChannels() { + return this.builder.readChannels; + } + + @Override + public String[] writeChannels() { + return this.builder.writeChannels; + } + } \ No newline at end of file diff --git a/io.openems.edge.controller.ess.fixstateofcharge/src/io/openems/edge/controller/ess/fixstateofcharge/ConfigFixStateOfCharge.java b/io.openems.edge.controller.ess.fixstateofcharge/src/io/openems/edge/controller/ess/fixstateofcharge/ConfigFixStateOfCharge.java index 18d14258634..386c51b0d9d 100644 --- a/io.openems.edge.controller.ess.fixstateofcharge/src/io/openems/edge/controller/ess/fixstateofcharge/ConfigFixStateOfCharge.java +++ b/io.openems.edge.controller.ess.fixstateofcharge/src/io/openems/edge/controller/ess/fixstateofcharge/ConfigFixStateOfCharge.java @@ -43,7 +43,7 @@ @AttributeDefinition(name = "Terminate time buffer in min", description = "Terminate itself after this time buffer. If zero is given, it will terminate instantly.") int terminationBuffer() default 0; - @AttributeDefinition(name = "Terminates itself after separate conditon", description = "Terminate itself after separate end condition given by the property endCondition.") + @AttributeDefinition(name = "Terminates itself after separate condition", description = "Terminate itself after separate end condition given by the property endCondition.") boolean conditionalTermination() default false; @AttributeDefinition(name = "Condition for termination", description = "Terminates itself if the conditionalTermination is true and this end condition was fulfilled.") diff --git a/io.openems.edge.controller.ess.fixstateofcharge/src/io/openems/edge/controller/ess/fixstateofcharge/ConfigPrepareBatteryExtension.java b/io.openems.edge.controller.ess.fixstateofcharge/src/io/openems/edge/controller/ess/fixstateofcharge/ConfigPrepareBatteryExtension.java index 3df7ef6eb6c..fe9358fea52 100644 --- a/io.openems.edge.controller.ess.fixstateofcharge/src/io/openems/edge/controller/ess/fixstateofcharge/ConfigPrepareBatteryExtension.java +++ b/io.openems.edge.controller.ess.fixstateofcharge/src/io/openems/edge/controller/ess/fixstateofcharge/ConfigPrepareBatteryExtension.java @@ -43,7 +43,7 @@ @AttributeDefinition(name = "Terminate time buffer in min", description = "Terminate itself after this time buffer. If zero is given, it will terminate instantly.") int terminationBuffer() default 120; - @AttributeDefinition(name = "Terminates itself after separate conditon", description = "Terminate itself after separate end condition given by the property endCondition.") + @AttributeDefinition(name = "Terminates itself after separate condition", description = "Terminate itself after separate end condition given by the property endCondition.") boolean conditionalTermination() default true; @AttributeDefinition(name = "Condition for termination", description = "Terminates itself if the conditionalTermination is true and this end condition was fulfilled.") diff --git a/io.openems.edge.controller.ess.fixstateofcharge/src/io/openems/edge/controller/ess/fixstateofcharge/api/ConfigProperties.java b/io.openems.edge.controller.ess.fixstateofcharge/src/io/openems/edge/controller/ess/fixstateofcharge/api/ConfigProperties.java index b3ce96e3939..8b92801164d 100644 --- a/io.openems.edge.controller.ess.fixstateofcharge/src/io/openems/edge/controller/ess/fixstateofcharge/api/ConfigProperties.java +++ b/io.openems.edge.controller.ess.fixstateofcharge/src/io/openems/edge/controller/ess/fixstateofcharge/api/ConfigProperties.java @@ -23,7 +23,7 @@ public class ConfigProperties { // Terminate time buffer in min private final int terminationBuffer; - // Terminates itself after separate conditon + // Terminates itself after separate condition private final boolean conditionalTermination; // Condition for termination diff --git a/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/Config.java b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/Config.java index 6a7c1da734c..69f598db15b 100644 --- a/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/Config.java +++ b/io.openems.edge.controller.ess.limiter14a/src/io/openems/edge/controller/ess/limiter14a/Config.java @@ -20,7 +20,7 @@ @AttributeDefinition(name = "Ess-ID", description = "ID of Ess.") String ess_id() default "ess0"; - @AttributeDefinition(name = "Input Channel", description = "When receiveing a signal, this channel triggers the execution of the limitation.") + @AttributeDefinition(name = "Input Channel", description = "When receiving a signal, this channel triggers the execution of the limitation.") String inputChannelAddress(); String webconsole_configurationFactory_nameHint() default "Controller Ess Limiter §14a [{id}]"; diff --git a/io.openems.edge.core/src/io/openems/edge/app/common/props/RelayProps.java b/io.openems.edge.core/src/io/openems/edge/app/common/props/RelayProps.java index 479a34f8fc3..f8275bd4fc8 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/common/props/RelayProps.java +++ b/io.openems.edge.core/src/io/openems/edge/app/common/props/RelayProps.java @@ -21,6 +21,7 @@ import io.openems.common.session.Language; import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.hardware.IoGpio; import io.openems.edge.common.channel.BooleanWriteChannel; import io.openems.edge.core.appmanager.AbstractOpenemsApp; import io.openems.edge.core.appmanager.AppDef; @@ -91,13 +92,19 @@ public static RelayContactInformation createPhaseInformation(// final List preferredRelays // ) { final var relayInfos = util.getAllRelayInfos(ComponentUtil.CORE_COMPONENT_IDS, // - component -> filter.stream().allMatch(t -> t.componentFilter().test(component)), // + component -> filter.stream() // + .map(RelayContactFilter::componentFilter) // + .filter(Objects::nonNull) // + .allMatch(t -> t.test(component)), // component -> filter.stream() // .map(RelayContactFilter::componentAliasMapper) // .filter(Objects::nonNull) // .map(t -> t.apply(component)) // .findAny().orElse(component.alias()), // - (component, channel) -> filter.stream().allMatch(t -> t.channelFilter().test(component, channel)), // + (component, channel) -> filter.stream() // + .map(RelayContactFilter::channelFilter) // + .filter(Objects::nonNull) // + .allMatch(t -> t.test(component, channel)), // (component, channel) -> filter.stream() // .map(RelayContactFilter::channelAliasMapper) // .filter(Objects::nonNull) // @@ -105,6 +112,7 @@ public static RelayContactInformation createPhaseInformation(// .findAny().orElse(channel.address().toString()), // (component, channel) -> filter.stream() // .map(RelayContactFilter::disabledReasons) // + .filter(Objects::nonNull) // .map(t -> t.apply(component, channel)) // .flatMap(Collection::stream) // .toList()); @@ -171,6 +179,15 @@ public static RelayContactFilter feneconHomeFilter(// ); } + /** + * Creates a {@link RelayContactFilter} for {@link IoGpio} components. + * + * @return the {@link RelayContactFilter} + */ + public static RelayContactFilter gpioFilter() { + return new RelayContactFilter(t -> !t.serviceFactoryPid().equals("IO.Gpio"), null, null, null, null); + } + /** * Creates the {@link PreferredRelay} if a Home 20/30 relay board is installed. * diff --git a/io.openems.edge.core/src/io/openems/edge/app/ess/Limiter14a.java b/io.openems.edge.core/src/io/openems/edge/app/ess/Limiter14a.java new file mode 100644 index 00000000000..8856960f7d7 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/ess/Limiter14a.java @@ -0,0 +1,178 @@ +package io.openems.edge.app.ess; + +import static io.openems.edge.app.common.props.CommonProps.alias; +import static java.util.Collections.emptyList; + +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.common.props.ComponentProps; +import io.openems.edge.app.common.props.RelayProps; +import io.openems.edge.app.common.props.RelayProps.RelayContactInformation; +import io.openems.edge.app.common.props.RelayProps.RelayContactInformationProvider; +import io.openems.edge.app.ess.Limiter14a.Limiter14aBundle; +import io.openems.edge.app.ess.Limiter14a.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter.BundleProvider; +import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; +import io.openems.edge.core.appmanager.dependency.DependencyUtil; +import io.openems.edge.core.appmanager.dependency.Tasks; +import io.openems.edge.core.appmanager.dependency.aggregatetask.SchedulerByCentralOrderConfiguration.SchedulerComponent; + +@Component(name = "App.Ess.Limiter14a") +public class Limiter14a extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public record Limiter14aBundle(// + ResourceBundle bundle, // + RelayContactInformation relayContactInformation // + ) implements BundleProvider, RelayContactInformationProvider { + + } + + public static enum Property implements Type { + CTRL_ESS_LIMITER_14A_ID(AppDef.componentId("ctrlEssLimiter14a0")), // + + ALIAS(alias()), // + ESS_ID(ComponentProps.pickManagedSymmetricEssId()), // + INPUT_CHANNEL_ADDRESS(RelayProps.relayContactDef(1)), // + ; + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, Limiter14aBundle> getParamter() { + return t -> new Limiter14aBundle(// + createResourceBundle(t.language), // + RelayProps.createPhaseInformation(t.app.componentUtil, 1, emptyList(), emptyList()) // + ); + } + + } + + @Activate + public Limiter14a(// + @Reference ComponentManager componentManager, // + ComponentContext componentContext, // + @Reference ConfigurationAdmin cm, // + @Reference ComponentUtil componentUtil // + ) { + super(componentManager, componentContext, cm, componentUtil); + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.ESS }; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE; + } + + @Override + protected Limiter14a getApp() { + return this; + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + + final var id = this.getId(t, p, Property.CTRL_ESS_LIMITER_14A_ID); + + final var alias = this.getString(p, Property.ALIAS); + final var essId = this.getString(p, Property.ESS_ID); + final var inputAddress = this.getString(p, Property.INPUT_CHANNEL_ADDRESS); + + final var components = List.of(// + new EdgeConfig.Component(id, alias, "Controller.Ess.Limiter14a", // + JsonUtils.buildJsonObject() // + .addProperty("ess.id", essId) // + .addProperty("inputChannelAddress", inputAddress) // + .build())); + + final var componentIdOfRelay = inputAddress.substring(0, inputAddress.indexOf('/')); + final var appIdOfRelay = DependencyUtil.getInstanceIdOfAppWhichHasComponent(this.componentManager, + componentIdOfRelay); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.schedulerByCentralOrder( + new SchedulerComponent(id, "Controller.Ess.Limiter14a", this.getAppId()))) // + .onlyIf(appIdOfRelay != null, c -> c.addDependency(new DependencyDeclaration("RELAY", // + DependencyDeclaration.CreatePolicy.NEVER, // + DependencyDeclaration.UpdatePolicy.NEVER, // + DependencyDeclaration.DeletePolicy.NEVER, // + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ALL, // + DependencyDeclaration.DependencyDeletePolicy.NOT_ALLOWED, // + DependencyDeclaration.AppDependencyConfig.create() // + .setSpecificInstanceId(appIdOfRelay) // + .build())))// + .build(); + }; + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create() // + .setCanSee(Role.ADMIN) // + .setCanDelete(Role.ADMIN) // + .build(); + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/hardware/IoGpio.java b/io.openems.edge.core/src/io/openems/edge/app/hardware/IoGpio.java new file mode 100644 index 00000000000..19720a1e148 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/hardware/IoGpio.java @@ -0,0 +1,141 @@ +package io.openems.edge.app.hardware; + +import static io.openems.edge.app.common.props.CommonProps.alias; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.hardware.IoGpio.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; + +@Component(name = "App.Hardware.IoGpio") +public class IoGpio extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public enum Property implements Type { + // TODO default value should start at 0 but for integrated systems and hardware + // apps to work properly this one starts at 1 + IO_ID(AppDef.componentId("io1")), // + + ALIAS(alias()), // + ; + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + + } + + @Activate + public IoGpio(// + @Reference ComponentManager componentManager, // + ComponentContext componentContext, // + @Reference ConfigurationAdmin cm, // + @Reference ComponentUtil componentUtil // + ) { + super(componentManager, componentContext, cm, componentUtil); + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.HARDWARE }; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE; + } + + @Override + protected IoGpio getApp() { + return this; + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + final var id = this.getId(t, p, Property.IO_ID); + + final var alias = this.getString(p, Property.ALIAS); + + final var components = List.of(// + new EdgeConfig.Component(id, alias, "IO.Gpio", JsonUtils.buildJsonObject() // + .addProperty("enabled", true) // + .addProperty("gpioPath", "/sys/class") // + .addProperty("hardwareType", "MODBERRY_X500_M40804_WB") // + .build())); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); + }; + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create() // + .setCanSee(Role.ADMIN) // + .setCanDelete(Role.ADMIN) // + .build(); + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/heat/CombinedHeatAndPower.java b/io.openems.edge.core/src/io/openems/edge/app/heat/CombinedHeatAndPower.java index cf84a43bc4f..3f1ca372b13 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/heat/CombinedHeatAndPower.java +++ b/io.openems.edge.core/src/io/openems/edge/app/heat/CombinedHeatAndPower.java @@ -122,7 +122,8 @@ public Function, CombinedHeatAndPowerPa return new CombinedHeatAndPowerParameter(// createResourceBundle(t.language), // createPhaseInformation(t.app.componentUtil, 1, // - List.of(RelayProps.feneconHomeFilter(t.language, isHomeInstalled, false)), // + List.of(RelayProps.feneconHomeFilter(t.language, isHomeInstalled, false), + RelayProps.gpioFilter()), // List.of(RelayProps.feneconHome2030PreferredRelays(isHomeInstalled, new int[] { 5 }), // PreferredRelay.of(4, new int[] { 1 }), // PreferredRelay.of(8, new int[] { 1 }))) // diff --git a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java index 3800b3065c6..1a7c9bcb76b 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java +++ b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java @@ -121,7 +121,8 @@ public Function, HeatPumpParameter> getParamter() { return new HeatPumpParameter(// createResourceBundle(t.language), // createPhaseInformation(t.app.componentUtil, 2, // - List.of(RelayProps.feneconHomeFilter(t.language, isHomeInstalled, false)), // + List.of(RelayProps.feneconHomeFilter(t.language, isHomeInstalled, false), + RelayProps.gpioFilter()), // List.of(RelayProps.feneconHome2030PreferredRelays(isHomeInstalled, new int[] { 5, 6 }), // PreferredRelay.of(4, new int[] { 2, 3 }), // PreferredRelay.of(8, new int[] { 2, 3 }))) // diff --git a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java index d816f514ce9..d483ac7ef6b 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java +++ b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java @@ -156,7 +156,8 @@ public Function, HeatingElementParameter> get return new HeatingElementParameter(// createResourceBundle(t.language), // createPhaseInformation(t.app.componentUtil, 3, // - List.of(RelayProps.feneconHomeFilter(t.language, isHomeInstalled, true)), // + List.of(RelayProps.feneconHomeFilter(t.language, isHomeInstalled, true), + RelayProps.gpioFilter()), // List.of(RelayProps.feneconHome2030PreferredRelays(isHomeInstalled, new int[] { 1, 2, 3 }), // PreferredRelay.of(4, new int[] { 1, 2, 3 }), // diff --git a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome.java b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome.java index 833c1a2d55c..790bcc230fb 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome.java +++ b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome.java @@ -3,6 +3,7 @@ import static io.openems.edge.app.common.props.CommonProps.alias; import static io.openems.edge.app.common.props.CommonProps.defaultDef; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.batteryInverter; +import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.essLimiter14aToHardware; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.gridOptimizedCharge; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.predictor; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.prepareBatteryExtension; @@ -14,6 +15,7 @@ import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.feedInType; import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.hasAcMeter; import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.hasEmergencyReserve; +import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.hasEssLimiter14a; import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.maxFeedInPower; import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.safetyCountry; import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.shadowManagementDisabled; @@ -54,6 +56,8 @@ import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.AppDef; import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.AppManagerUtil; +import io.openems.edge.core.appmanager.AppManagerUtilSupplier; import io.openems.edge.core.appmanager.ComponentUtil; import io.openems.edge.core.appmanager.ConfigurationTarget; import io.openems.edge.core.appmanager.OpenemsApp; @@ -118,7 +122,7 @@ */ @Component(name = "App.FENECON.Home") public class FeneconHome extends AbstractOpenemsAppWithProps - implements OpenemsApp { + implements OpenemsApp, AppManagerUtilSupplier { public record FeneconHomeParameter(// ResourceBundle bundle, // @@ -162,7 +166,7 @@ public static enum Property implements Type { return new JsonPrimitive(parameter.defaultValues().feedInSetting()); }))), // - + HAS_ESS_LIMITER_14A(hasEssLimiter14a()), // // External AC PV HAS_AC_METER(AppDef.copyOfGeneric(hasAcMeter(), def -> def // .setDefaultValue((app, property, l, parameter) -> { @@ -248,10 +252,18 @@ public Function, FeneconHomeParameter> getParamt } } + private final AppManagerUtil appManagerUtil; + @Activate - public FeneconHome(@Reference ComponentManager componentManager, ComponentContext context, - @Reference ConfigurationAdmin cm, @Reference ComponentUtil componentUtil) { + public FeneconHome(// + @Reference final ComponentManager componentManager, // + final ComponentContext context, // + @Reference final ConfigurationAdmin cm, // + @Reference final ComponentUtil componentUtil, // + @Reference final AppManagerUtil appManagerUtil // + ) { super(componentManager, context, cm, componentUtil); + this.appManagerUtil = appManagerUtil; } @Override @@ -280,6 +292,8 @@ AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { : 0; final var shadowManagmentDisabled = this.getBoolean(p, Property.SHADOW_MANAGEMENT_DISABLED); + + final var hasEssLimiter14a = this.getBoolean(p, Property.HAS_ESS_LIMITER_14A); final var hasAcMeter = this.getBoolean(p, Property.HAS_AC_METER); // for older versions this property is undefined final var acType = this.getEnum(p, AcMeterType.class, Property.AC_METER_TYPE); @@ -420,6 +434,13 @@ AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { dependencies.add(acType.getDependency(modbusIdExternal)); } + if (hasEssLimiter14a) { + final var dependency = essLimiter14aToHardware(this.appManagerUtil); + if (dependency != null) { + dependencies.add(dependency); + } + } + final var schedulerComponents = new ArrayList(); if (hasEmergencyReserve) { schedulerComponents.add(new SchedulerComponent("ctrlEmergencyCapacityReserve0", @@ -539,4 +560,9 @@ private static Optional getBatteryInverter(ComponentManage return batteryInverter; } + @Override + public AppManagerUtil getAppManagerUtil() { + return this.appManagerUtil; + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome20.java b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome20.java index 083bd11673b..e557fd7ebb4 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome20.java +++ b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome20.java @@ -10,6 +10,7 @@ import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.ctrlEssSurplusFeedToGrid; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.emergencyMeter; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.ess; +import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.essLimiter14aToHardware; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.gridMeter; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.gridOptimizedCharge; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.io; @@ -29,6 +30,7 @@ import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.gridMeterType; import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.hasAcMeter; import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.hasEmergencyReserve; +import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.hasEssLimiter14a; import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.maxFeedInPower; import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.safetyCountry; import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.shadowManagementDisabled; @@ -66,6 +68,8 @@ import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.AppDef; import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.AppManagerUtil; +import io.openems.edge.core.appmanager.AppManagerUtilSupplier; import io.openems.edge.core.appmanager.ComponentUtil; import io.openems.edge.core.appmanager.ConfigurationTarget; import io.openems.edge.core.appmanager.OpenemsApp; @@ -134,7 +138,7 @@ */ @Component(name = "App.FENECON.Home.20") public class FeneconHome20 extends AbstractOpenemsAppWithProps - implements OpenemsApp { + implements OpenemsApp, AppManagerUtilSupplier { public enum Property implements PropertyParent { ALIAS(alias()), // @@ -149,6 +153,8 @@ public enum Property implements PropertyParent { GRID_METER_CATEGORY(gridMeterType()), // CT_RATIO_FIRST(ctRatioFirst(GRID_METER_CATEGORY)), // + HAS_ESS_LIMITER_14A(hasEssLimiter14a()), // + HAS_AC_METER(hasAcMeter()), // AC_METER_TYPE(acMeterType(HAS_AC_METER)), // @@ -194,15 +200,18 @@ public Type self() { private static final IntFunction MPPT_ALIAS = value -> "ALIAS_MPPT_" + (value + 1); private final Map pvDefs = new TreeMap<>(); + private final AppManagerUtil appManagerUtil; @Activate public FeneconHome20(// @Reference final ComponentManager componentManager, // final ComponentContext componentContext, // @Reference final ConfigurationAdmin cm, // - @Reference final ComponentUtil componentUtil // + @Reference final ComponentUtil componentUtil, // + @Reference final AppManagerUtil appManagerUtil // ) { super(componentManager, componentContext, cm, componentUtil); + this.appManagerUtil = appManagerUtil; BooleanExpression anyOldPvSelected = null; for (int i = 0; i < MAX_NUMBER_OF_PV; i++) { @@ -287,6 +296,7 @@ protected ThrowingTriFunction(); if (hasEmergencyReserve) { schedulerComponents.add(new SchedulerComponent("ctrlEmergencyCapacityReserve0", @@ -421,6 +436,11 @@ protected PropertyParent[] propertyValues() { return builder.build().toArray(PropertyParent[]::new); } + @Override + public AppManagerUtil getAppManagerUtil() { + return this.appManagerUtil; + } + public static interface PropertyParent extends Type { } diff --git a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome30.java b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome30.java index 34d2c672789..019e73af9c2 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome30.java +++ b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome30.java @@ -10,6 +10,7 @@ import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.ctrlEssSurplusFeedToGrid; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.emergencyMeter; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.ess; +import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.essLimiter14aToHardware; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.gridMeter; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.gridOptimizedCharge; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.io; @@ -29,6 +30,7 @@ import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.gridMeterType; import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.hasAcMeter; import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.hasEmergencyReserve; +import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.hasEssLimiter14a; import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.maxFeedInPower; import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.safetyCountry; import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.shadowManagementDisabled; @@ -66,6 +68,8 @@ import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.AppDef; import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.AppManagerUtil; +import io.openems.edge.core.appmanager.AppManagerUtilSupplier; import io.openems.edge.core.appmanager.ComponentUtil; import io.openems.edge.core.appmanager.ConfigurationTarget; import io.openems.edge.core.appmanager.OpenemsApp; @@ -134,7 +138,7 @@ */ @Component(name = "App.FENECON.Home.30") public class FeneconHome30 extends AbstractOpenemsAppWithProps - implements OpenemsApp { + implements OpenemsApp, AppManagerUtilSupplier { public enum Property implements PropertyParent { ALIAS(alias()), // @@ -149,6 +153,8 @@ public enum Property implements PropertyParent { GRID_METER_CATEGORY(gridMeterType()), // CT_RATIO_FIRST(ctRatioFirst(GRID_METER_CATEGORY)), // + HAS_ESS_LIMITER_14A(hasEssLimiter14a()), // + HAS_AC_METER(hasAcMeter()), // AC_METER_TYPE(acMeterType(HAS_AC_METER)), // @@ -194,15 +200,18 @@ public Type self() { private static final IntFunction MPPT_ALIAS = value -> "ALIAS_MPPT_" + (value + 1); private final Map pvDefs = new TreeMap<>(); + private final AppManagerUtil appManagerUtil; @Activate public FeneconHome30(// @Reference final ComponentManager componentManager, // final ComponentContext componentContext, // @Reference final ConfigurationAdmin cm, // - @Reference final ComponentUtil componentUtil // + @Reference final ComponentUtil componentUtil, // + @Reference final AppManagerUtil appManagerUtil // ) { super(componentManager, componentContext, cm, componentUtil); + this.appManagerUtil = appManagerUtil; BooleanExpression anyOldPvSelected = null; for (int i = 0; i < MAX_NUMBER_OF_PV; i++) { @@ -288,6 +297,7 @@ protected ThrowingTriFunction(); if (hasEmergencyReserve) { schedulerComponents.add(new SchedulerComponent("ctrlEmergencyCapacityReserve0", @@ -423,6 +437,11 @@ protected PropertyParent[] propertyValues() { return builder.build().toArray(PropertyParent[]::new); } + @Override + public AppManagerUtil getAppManagerUtil() { + return this.appManagerUtil; + } + public static interface PropertyParent extends Type { } diff --git a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHomeComponents.java b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHomeComponents.java index 3a0b44135d9..72f62c234ba 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHomeComponents.java +++ b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHomeComponents.java @@ -2,16 +2,22 @@ import java.util.ResourceBundle; +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.exceptions.OpenemsException; import io.openems.common.types.EdgeConfig; import io.openems.common.types.EdgeConfig.Component; import io.openems.common.utils.JsonUtils; import io.openems.edge.app.enums.FeedInType; import io.openems.edge.app.enums.Parity; import io.openems.edge.app.enums.SafetyCountry; +import io.openems.edge.app.ess.Limiter14a; import io.openems.edge.app.ess.PrepareBatteryExtension; +import io.openems.edge.app.hardware.IoGpio; import io.openems.edge.app.pvselfconsumption.GridOptimizedCharge; import io.openems.edge.app.pvselfconsumption.SelfConsumptionOptimization; +import io.openems.edge.core.appmanager.AppManagerUtil; import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsAppCategory; import io.openems.edge.core.appmanager.TranslationUtil; import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; @@ -480,6 +486,60 @@ public static DependencyDeclaration prepareBatteryExtension() { .build()); } + /** + * Creates a default essLimiter14a dependency for a FENECON Home. + * + * @param ioId the id of the input component + * @return the {@link DependencyDeclaration} + */ + public static DependencyDeclaration essLimiter14a(// + final String ioId // + ) { + return new DependencyDeclaration("ESS_LIMITER_14A", // + DependencyDeclaration.CreatePolicy.IF_NOT_EXISTING, // + DependencyDeclaration.UpdatePolicy.NEVER, // + DependencyDeclaration.DeletePolicy.IF_MINE, // + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ONLY_UNCONFIGURED_PROPERTIES, // + DependencyDeclaration.DependencyDeletePolicy.NOT_ALLOWED, // + DependencyDeclaration.AppDependencyConfig.create() // + .setAppId("App.Ess.Limiter14a") // + .setProperties(JsonUtils.buildJsonObject() // + .addProperty(Limiter14a.Property.ESS_ID.name(), "ess0") // + .addProperty(Limiter14a.Property.INPUT_CHANNEL_ADDRESS.name(), ioId + "/DigitalInput1") // + .build()) // + .build()); + } + + /** + * Creates a default essLimiter14a dependency for a FENECON Home which can be + * different depending on the hardware type. + * + * @param appManagerUtil the {@link AppManagerUtil} to get the hardware type + * @return the {@link DependencyDeclaration} of the specific hardware or null if + * not specified for the current hardware + * @throws OpenemsNamedException on error + */ + public static DependencyDeclaration essLimiter14aToHardware(AppManagerUtil appManagerUtil) + throws OpenemsNamedException { + final var deviceHardware = appManagerUtil + .getInstantiatedAppsByCategories(OpenemsAppCategory.OPENEMS_DEVICE_HARDWARE); + final var app = deviceHardware.stream().findAny().orElse(null); + switch (app == null ? "" : app.appId) { + case "App.OpenemsHardware.CM3", "App.OpenemsHardware.CM4S" -> { + for (var dependency : app.dependencies) { + if (!"IO_GPIO".equals(dependency.key)) { + continue; + } + final var instance = appManagerUtil.findInstanceByIdOrError(dependency.instanceId); + final var ioId = instance.properties.get(IoGpio.Property.IO_ID.name()).getAsString(); + return essLimiter14a(ioId); + } + } + } + throw new OpenemsException( + "Hardware " + (app == null ? "UNDEFINED" : app.appId) + " not supported for ess limiter 14a."); + } + private FeneconHomeComponents() { } diff --git a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/IntegratedSystemProps.java b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/IntegratedSystemProps.java index d5610be64fd..4170c49226b 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/IntegratedSystemProps.java +++ b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/IntegratedSystemProps.java @@ -12,8 +12,10 @@ import io.openems.edge.app.enums.OptionsFactory; import io.openems.edge.app.enums.SafetyCountry; import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppManagerUtilSupplier; import io.openems.edge.core.appmanager.Nameable; import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCategory; import io.openems.edge.core.appmanager.Type.Parameter.BundleProvider; import io.openems.edge.core.appmanager.formly.Exp; import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; @@ -271,6 +273,34 @@ public static final AppDef acMeterType(// })); } + /** + * Creates a {@link AppDef} for selecting if the system has a ess limiter for + * 14a. + * + * @param the type of the app + * @return the created {@link AppDef} + */ + public static final // + AppDef hasEssLimiter14a() { + return AppDef.copyOfGeneric(defaultDef(), def -> def // + .setTranslatedLabel("App.IntegratedSystem.hasEssLimiter14a.label") // + .setDefaultValue(false) // + .setField(JsonFormlyUtil::buildCheckboxFromNameable, (app, property, l, parameter, field) -> { + final var hardwareTypes = app.getAppManagerUtil() + .getInstantiatedAppsByCategories(OpenemsAppCategory.OPENEMS_DEVICE_HARDWARE); + + final var isSupported = hardwareTypes.stream()// + .anyMatch(t -> switch (t.appId) { + case "App.OpenemsHardware.CM3", "App.OpenemsHardware.CM4S" -> true; + default -> false; + }); + + if (!isSupported) { + field.disabled(true); + } + })); + } + private IntegratedSystemProps() { } diff --git a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/fenecon/commercial/FeneconCommercial92.java b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/fenecon/commercial/FeneconCommercial92.java new file mode 100644 index 00000000000..0bc05c28b03 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/fenecon/commercial/FeneconCommercial92.java @@ -0,0 +1,175 @@ +package io.openems.edge.app.integratedsystem.fenecon.commercial; + +import static io.openems.edge.app.common.props.CommonProps.alias; +import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.feedInType; +import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.maxFeedInPower; +import static io.openems.edge.app.integratedsystem.IntegratedSystemProps.safetyCountry; + +import java.util.Map; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.common.collect.Lists; +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.edge.app.enums.FeedInType; +import io.openems.edge.app.integratedsystem.FeneconHomeComponents; +import io.openems.edge.app.integratedsystem.fenecon.commercial.FeneconCommercial92.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.InterfaceConfiguration; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; + +@Component(name = "App.FENECON.Commercial.92") +public class FeneconCommercial92 extends + AbstractOpenemsAppWithProps implements OpenemsApp { + + public enum Property implements Type { + ALIAS(alias()), // + + SAFETY_COUNTRY(AppDef.copyOfGeneric(safetyCountry(), def -> def // + .setRequired(true))), // + + FEED_IN_TYPE(feedInType(FeedInType.EXTERNAL_LIMITATION)), // + MAX_FEED_IN_POWER(maxFeedInPower(FEED_IN_TYPE)), // + ; + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + + } + + @Activate + public FeneconCommercial92(// + @Reference final ComponentManager componentManager, // + final ComponentContext componentContext, // + @Reference final ConfigurationAdmin cm, // + @Reference final ComponentUtil componentUtil // + ) { + super(componentManager, componentContext, cm, componentUtil); + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.INTEGRATED_SYSTEM }; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE_IN_CATEGORY; + } + + @Override + protected FeneconCommercial92 getApp() { + return this; + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + final var bundle = AbstractOpenemsApp.getTranslationBundle(l); + + final var batteryId = "battery0"; + final var batteryInverterId = "batteryInverter0"; + final var modbusToBatteryId = "modbus0"; + final var modbusToBatteryInverterId = "modbus1"; + final var modbusToGridMeterId = "modbus2"; + final var modbusToExternalDevicesId = "modbus3"; + final var gridMeterId = "meter0"; + final var essId = "ess0"; + + final var feedInType = this.getEnum(p, FeedInType.class, Property.FEED_IN_TYPE); + final var maxFeedInPower = feedInType == FeedInType.DYNAMIC_LIMITATION + ? this.getInt(p, Property.MAX_FEED_IN_POWER) + : 0; + + final var components = Lists.newArrayList(// + FeneconHomeComponents.battery(bundle, batteryId, modbusToBatteryId), // + FeneconCommercialComponents.batteryInverter(bundle, batteryInverterId, modbusToBatteryInverterId), // + FeneconHomeComponents.ess(bundle, essId, batteryId, batteryInverterId), // + FeneconHomeComponents.io(bundle, modbusToBatteryId), // + FeneconHomeComponents.modbusInternal(bundle, t, modbusToBatteryId), // + FeneconHomeComponents.predictor(bundle, t), // + FeneconCommercialComponents.modbusToBatteryInverter(bundle, t, modbusToBatteryInverterId), // + FeneconCommercialComponents.modbusToGridMeter(bundle, t, modbusToGridMeterId), // + FeneconHomeComponents.modbusForExternalMeters(bundle, t, modbusToExternalDevicesId) // + ); + + final var dependencies = Lists.newArrayList(// + FeneconHomeComponents.selfConsumptionOptimization(t, essId, gridMeterId), // + FeneconHomeComponents.gridOptimizedCharge(t, feedInType, maxFeedInPower), // + FeneconHomeComponents.prepareBatteryExtension(), // + FeneconCommercialComponents.gridMeter(bundle, gridMeterId, modbusToGridMeterId) // + ); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.staticIp(new InterfaceConfiguration("eth1") // + .addIp("BatteryInverter", "172.16.0.99/24"))) + .addDependencies(dependencies) // + .build(); + }; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create() // + .setCanDelete(Role.INSTALLER) // + .setCanSee(Role.INSTALLER) // + .build(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/fenecon/commercial/FeneconCommercialComponents.java b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/fenecon/commercial/FeneconCommercialComponents.java new file mode 100644 index 00000000000..f4857fd1153 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/fenecon/commercial/FeneconCommercialComponents.java @@ -0,0 +1,130 @@ +package io.openems.edge.app.integratedsystem.fenecon.commercial; + +import static io.openems.edge.core.appmanager.TranslationUtil.translate; + +import java.util.ResourceBundle; + +import io.openems.common.types.EdgeConfig; +import io.openems.common.types.EdgeConfig.Component; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.enums.MeterType; +import io.openems.edge.app.enums.Parity; +import io.openems.edge.app.meter.KdkMeter; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; + +public final class FeneconCommercialComponents { + + /** + * Creates a default battery inverter component for a FENECON Commercial 92. + * + * @param bundle the translation bundle + * @param batteryInverterId the id of the battery inverter + * @param modbusId the id of the modbus bridge + * @return the {@link Component} + */ + public static EdgeConfig.Component batteryInverter(// + final ResourceBundle bundle, // + final String batteryInverterId, // + final String modbusId // + ) { + return new EdgeConfig.Component(batteryInverterId, + translate(bundle, "App.IntegratedSystem.batteryInverter0.alias"), + "Battery-Inverter.Kaco.BlueplanetGridsave", // + JsonUtils.buildJsonObject() // + .addProperty("enabled", true) // + .addProperty("modbus.id", modbusId) // + .addProperty("startStop", "AUTO") // + .build()); + } + + /** + * Creates a default modbus bridge component to the battery inverter for a + * FENECON Commercial 92. + * + * @param bundle the translation bundle + * @param t the current {@link ConfigurationTarget} + * @param modbusId the id of the modbus bridge + * @return the {@link Component} + */ + public static EdgeConfig.Component modbusToBatteryInverter(// + final ResourceBundle bundle, // + final ConfigurationTarget t, // + final String modbusId // + ) { + return new EdgeConfig.Component(modbusId, translate(bundle, "App.IntegratedSystem.modbus1.alias"), + "Bridge.Modbus.Tcp", // + JsonUtils.buildJsonObject() // + .addProperty("enabled", true) // + .addProperty("ip", "172.16.0.100") // + .addProperty("port", 502) // + .onlyIf(t == ConfigurationTarget.ADD, b -> b// + .addProperty("invalidateElementsAfterReadErrors", 1) // + .addProperty("logVerbosity", "NONE")) + .build()); + } + + /** + * Creates a default modbus bridge component to the grid meter for a FENECON + * Commercial 92. + * + * @param bundle the translation bundle + * @param t the current {@link ConfigurationTarget} + * @param modbusId the id of the external modbus bridge + * @return the {@link Component} + */ + public static EdgeConfig.Component modbusToGridMeter(// + final ResourceBundle bundle, // + final ConfigurationTarget t, // + final String modbusId // + ) { + return new EdgeConfig.Component(modbusId, translate(bundle, "App.IntegratedSystem.modbusToGridMeter.alias"), + "Bridge.Modbus.Serial", // + JsonUtils.buildJsonObject() // + .addProperty("enabled", true) // + .addProperty("baudRate", 9600) // + .addProperty("databits", 8) // + .addProperty("parity", Parity.NONE) // + .addProperty("portName", "/dev/busUSB2") // + .addProperty("stopbits", "ONE") // + .onlyIf(t == ConfigurationTarget.ADD, b -> b// + .addProperty("invalidateElementsAfterReadErrors", 1) // + .addProperty("logVerbosity", "NONE")) + .build()); + } + + /** + * Creates a default gridMeter dependency for a FENECON Commercial 92. + * + * @param bundle the translation bundle + * @param gridMeterId the id of the grid meter + * @param modbusId the id of the modbus bridge + * @return the {@link DependencyDeclaration} + */ + public static DependencyDeclaration gridMeter(// + final ResourceBundle bundle, // + final String gridMeterId, // + final String modbusId // + ) { + return new DependencyDeclaration("GRID_METER", // + DependencyDeclaration.CreatePolicy.IF_NOT_EXISTING, // + DependencyDeclaration.UpdatePolicy.ALWAYS, // + DependencyDeclaration.DeletePolicy.IF_MINE, // + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ONLY_UNCONFIGURED_PROPERTIES, // + DependencyDeclaration.DependencyDeletePolicy.NOT_ALLOWED, // + DependencyDeclaration.AppDependencyConfig.create() // + .setAppId("App.Meter.Kdk") // + .setAlias(translate(bundle, "App.Meter.gridMeter")) // + .setProperties(JsonUtils.buildJsonObject() // + .addProperty(KdkMeter.Property.METER_ID.name(), gridMeterId) // + .addProperty(KdkMeter.Property.MODBUS_ID.name(), modbusId) // + .addProperty(KdkMeter.Property.MODBUS_UNIT_ID.name(), 5) // + .addProperty(KdkMeter.Property.TYPE.name(), MeterType.GRID) // + .build()) + .build()); + } + + private FeneconCommercialComponents() { + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/loadcontrol/ManualRelayControl.java b/io.openems.edge.core/src/io/openems/edge/app/loadcontrol/ManualRelayControl.java index b10ae500aae..eac82b5b982 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/loadcontrol/ManualRelayControl.java +++ b/io.openems.edge.core/src/io/openems/edge/app/loadcontrol/ManualRelayControl.java @@ -114,7 +114,8 @@ public Function, ManualRelayControlParame return new ManualRelayControlParameter(// createResourceBundle(t.language), // createPhaseInformation(t.app.componentUtil, 2, // - List.of(RelayProps.feneconHomeFilter(t.language, isHomeInstalled, true)), // + List.of(RelayProps.feneconHomeFilter(t.language, isHomeInstalled, true), + RelayProps.gpioFilter()), // List.of(PreferredRelay.of(4, new int[] { 1 }), // PreferredRelay.of(8, new int[] { 1 }))) // ); diff --git a/io.openems.edge.core/src/io/openems/edge/app/loadcontrol/ThresholdControl.java b/io.openems.edge.core/src/io/openems/edge/app/loadcontrol/ThresholdControl.java index 00b93cd7aba..23e3d80be1c 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/loadcontrol/ThresholdControl.java +++ b/io.openems.edge.core/src/io/openems/edge/app/loadcontrol/ThresholdControl.java @@ -115,7 +115,8 @@ public Function, ThresholdControlControlPar return new ThresholdControlControlParameter(// createResourceBundle(t.language), // createPhaseInformation(t.app.componentUtil, 2, // - List.of(RelayProps.feneconHomeFilter(t.language, isHomeInstalled, true)), // + List.of(RelayProps.feneconHomeFilter(t.language, isHomeInstalled, true), + RelayProps.gpioFilter()), // List.of(PreferredRelay.of(4, new int[] { 1 }), // PreferredRelay.of(8, new int[] { 1 }))) // ); diff --git a/io.openems.edge.core/src/io/openems/edge/app/meter/JanitzaMeter.java b/io.openems.edge.core/src/io/openems/edge/app/meter/JanitzaMeter.java index ac824238ce5..98b14f0e69b 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/meter/JanitzaMeter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/meter/JanitzaMeter.java @@ -43,6 +43,7 @@ import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCardinality; import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.TranslationUtil; import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; @@ -168,7 +169,7 @@ protected ThrowingTriFunction, L final var meterId = this.getId(t, p, Property.METER_ID, "meter1"); final var alias = this.getString(p, l, Property.ALIAS); - final var factorieId = this.getString(p, Property.MODEL); + final var factoryId = this.getString(p, Property.MODEL); final var type = this.getEnum(p, MeterType.class, Property.TYPE); final var modbusUnitId = this.getInt(p, Property.MODBUS_UNIT_ID); final var integrationType = this.getEnum(p, ModbusType.class, Property.INTEGRATION_TYPE); @@ -182,7 +183,9 @@ protected ThrowingTriFunction, L final var port = this.getInt(p, Property.PORT); final var tcpModbusId = this.getId(t, p, Property.MODBUS_ID); - components.add(new EdgeConfig.Component(tcpModbusId, "bridge", "Bridge.Modbus.Tcp", // + components.add(new EdgeConfig.Component(tcpModbusId, + TranslationUtil.translate(AbstractOpenemsApp.getTranslationBundle(l), "App.Meter.alias"), + "Bridge.Modbus.Tcp", // JsonUtils.buildJsonObject() // .addProperty("ip", ip) // .addProperty("port", port) // @@ -192,7 +195,7 @@ protected ThrowingTriFunction, L } }; - components.add(new EdgeConfig.Component(meterId, alias, factorieId, // + components.add(new EdgeConfig.Component(meterId, alias, factoryId, // JsonUtils.buildJsonObject() // .addProperty("modbus.id", modbusId) // .addProperty("modbusUnitId", modbusUnitId) // diff --git a/io.openems.edge.core/src/io/openems/edge/app/meter/PhoenixContactMeter.java b/io.openems.edge.core/src/io/openems/edge/app/meter/PhoenixContactMeter.java new file mode 100644 index 00000000000..82fd0866bd1 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/meter/PhoenixContactMeter.java @@ -0,0 +1,169 @@ +package io.openems.edge.app.meter; + +import java.util.Map; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.common.collect.Lists; +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.common.props.CommonProps; +import io.openems.edge.app.enums.MeterType; +import io.openems.edge.app.meter.PhoenixContactMeter.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; + +/** + * Describes a App for a PhoenixContact meter. + */ +@Component(name = "App.Meter.PhoenixContact") +public class PhoenixContactMeter extends + AbstractOpenemsAppWithProps implements OpenemsApp { + + public enum Property implements Type { + // Component-IDs + METER_ID(AppDef.componentId("meter1")), // + MODBUS_ID(AppDef.componentId("modbus2")), // + // Properties + ALIAS(CommonProps.alias()), // + TYPE(AppDef.copyOfGeneric(MeterProps.type(MeterType.GRID), def -> def // + .setRequired(true))), // + IP(MeterProps.ip() // + .setRequired(true)), // + PORT(MeterProps.port() // + .setRequired(true)), // + MODBUS_UNIT_ID(AppDef.copyOfGeneric(MeterProps.modbusUnitId(), def -> def // + .setRequired(true) // + .setDefaultValue(1))), // + ; + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + @Activate + public PhoenixContactMeter(// + @Reference final ComponentManager componentManager, // + final ComponentContext componentContext, // + @Reference final ConfigurationAdmin cm, // + @Reference final ComponentUtil componentUtil // + ) { + super(componentManager, componentContext, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + final var meterId = this.getId(t, p, Property.METER_ID); + + final var alias = this.getString(p, l, Property.ALIAS); + final var type = this.getEnum(p, MeterType.class, Property.TYPE); + final var modbusUnitId = this.getInt(p, Property.MODBUS_UNIT_ID); + + final var ip = this.getString(p, Property.IP); + final var port = this.getInt(p, Property.PORT); + final var modbusId = this.getId(t, p, Property.MODBUS_ID); + + final var components = Lists.newArrayList(// + new EdgeConfig.Component(modbusId, + TranslationUtil.translate(AbstractOpenemsApp.getTranslationBundle(l), "App.Meter.alias"), + "Bridge.Modbus.Tcp", // + JsonUtils.buildJsonObject() // + .addProperty("ip", ip) // + .addProperty("port", port) // + .build()), // + new EdgeConfig.Component(meterId, alias, "Meter.PhoenixContact", // + JsonUtils.buildJsonObject() // + .addProperty("modbus.id", modbusId) // + .addProperty("modbusUnitId", modbusUnitId) // + .addProperty("type", type) // + .build()) // + ); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); + }; + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public final OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.METER }; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.MULTIPLE; + } + + @Override + protected PhoenixContactMeter getApp() { + return this; + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create()// + .setCanSee(Role.ADMIN)// + .setCanDelete(Role.ADMIN) // + .build(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/meter/PqPlusMeter.java b/io.openems.edge.core/src/io/openems/edge/app/meter/PqPlusMeter.java new file mode 100644 index 00000000000..064eef6f63a --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/meter/PqPlusMeter.java @@ -0,0 +1,279 @@ +package io.openems.edge.app.meter; + +import static io.openems.edge.app.common.props.CommonProps.defaultDef; +import static io.openems.edge.core.appmanager.TranslationUtil.translate; + +import java.util.ArrayList; +import java.util.Map; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.common.props.CommonProps; +import io.openems.edge.app.common.props.CommunicationProps; +import io.openems.edge.app.common.props.ComponentProps; +import io.openems.edge.app.common.props.PropsUtil; +import io.openems.edge.app.enums.MeterType; +import io.openems.edge.app.enums.ModbusType; +import io.openems.edge.app.enums.OptionsFactory; +import io.openems.edge.app.enums.TranslatableEnum; +import io.openems.edge.app.meter.PqPlusMeter.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.AppManagerUtil; +import io.openems.edge.core.appmanager.AppManagerUtilSupplier; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ComponentUtilSupplier; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; +import io.openems.edge.core.appmanager.formly.Exp; +import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; + +/** + * Describes a App for a PQ-Plus meter. + * + *
    +  {
    +    "appId":"App.Meter.PqPlus",
    +    "alias":"PQ-Plus Meter",
    +    "instanceId": UUID,
    +    "image": base64,
    +    "properties":{
    +    	"METER_ID": "meter1",
    +    	"TYPE": "PRODUCTION",
    +    	"MODBUS_ID": "modbus1",
    +    	"MODBUS_UNIT_ID": 6
    +    },
    +    "appDescriptor": {
    +    	"websiteUrl": {@link AppDescriptor#getWebsiteUrl()}
    +    }
    +  }
    + * 
    + */ +@Component(name = "App.Meter.PqPlus") +public class PqPlusMeter extends AbstractOpenemsAppWithProps + implements OpenemsApp, ComponentUtilSupplier, AppManagerUtilSupplier { + + public enum Property implements Type { + // Component-IDs + METER_ID(AppDef.componentId("meter1")), // + MODBUS_ID(AppDef.componentId("modbus2")), // + // Properties + ALIAS(CommonProps.alias()), // + TYPE(AppDef.copyOfGeneric(MeterProps.type(MeterType.GRID), def -> def // + .setRequired(true))), // + INTEGRATION_TYPE(CommunicationProps.modbusType() // + .setRequired(true)), // + IP(MeterProps.ip() // + .setDefaultValue("10.4.0.12") // + .setRequired(true) // + .wrapField((app, property, l, parameter, field) -> { + field.onlyShowIf((Exp.currentModelValue(INTEGRATION_TYPE) // + .equal(Exp.staticValue(ModbusType.TCP)))); + })), // + PORT(MeterProps.port() // + .setRequired(true) // + .wrapField((app, property, l, parameter, field) -> { + field.onlyShowIf((Exp.currentModelValue(INTEGRATION_TYPE) // + .equal(Exp.staticValue(ModbusType.TCP)))); + })), // + SELECTED_MODBUS_ID(AppDef.copyOfGeneric(ComponentProps.pickSerialModbusId(), def -> def // + .setRequired(true) // + .wrapField((app, property, l, parameter, field) -> { + if (PropsUtil.isHomeInstalled(app.getAppManagerUtil())) { + field.readonly(true); + } + field.onlyShowIf(Exp.currentModelValue(INTEGRATION_TYPE) // + .equal(Exp.staticValue(ModbusType.RTU))); + })) // + .setAutoGenerateField(false)), // + MODEL(AppDef.copyOfGeneric(defaultDef(), def -> def // + .setTranslatedLabelWithAppPrefix(".productModel") // + .setDefaultValue(PqPlusModel.UMD_96.getValue()) // + .setRequired(true) // + .setField(JsonFormlyUtil::buildSelect, (app, property, l, parameter, field) -> { + field.setOptions(OptionsFactory.of(PqPlusModel.class), l); + }))), // + MODBUS_UNIT_ID(AppDef.copyOfGeneric(MeterProps.modbusUnitId(), def -> def // + .setRequired(true) // + .setAutoGenerateField(false) // + .setDefaultValue(6))), // + MODBUS_GROUP(CommunicationProps.modbusGroup(// + SELECTED_MODBUS_ID, SELECTED_MODBUS_ID.def(), // + MODBUS_UNIT_ID, MODBUS_UNIT_ID.def(), INTEGRATION_TYPE)), // + ; + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + private final AppManagerUtil appManagerUtil; + + @Activate + public PqPlusMeter(// + @Reference final ComponentManager componentManager, // + final ComponentContext componentContext, // + @Reference final ConfigurationAdmin cm, // + @Reference final ComponentUtil componentUtil, // + @Reference final AppManagerUtil appManagerUtil // + ) { + super(componentManager, componentContext, cm, componentUtil); + this.appManagerUtil = appManagerUtil; + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + final var meterId = this.getId(t, p, Property.METER_ID); + + final var alias = this.getString(p, l, Property.ALIAS); + final var factoryId = this.getString(p, Property.MODEL); + final var type = this.getEnum(p, MeterType.class, Property.TYPE); + final var modbusUnitId = this.getInt(p, Property.MODBUS_UNIT_ID); + final var integrationType = this.getEnum(p, ModbusType.class, Property.INTEGRATION_TYPE); + + final var components = new ArrayList(); + + final var modbusId = switch (integrationType) { + case RTU -> this.getString(p, Property.SELECTED_MODBUS_ID); + case TCP -> { + final var ip = this.getString(p, Property.IP); + final var port = this.getInt(p, Property.PORT); + final var tcpModbusId = this.getId(t, p, Property.MODBUS_ID); + + components.add(new EdgeConfig.Component(tcpModbusId, + TranslationUtil.translate(AbstractOpenemsApp.getTranslationBundle(l), "App.Meter.alias"), + "Bridge.Modbus.Tcp", // + JsonUtils.buildJsonObject() // + .addProperty("ip", ip) // + .addProperty("port", port) // + .build())); + + yield tcpModbusId; + } + }; + + components.add(// + new EdgeConfig.Component(meterId, alias, factoryId, // + JsonUtils.buildJsonObject() // + .addProperty("modbus.id", modbusId) // + .addProperty("modbusUnitId", modbusUnitId) // + .addProperty("type", type) // + .build()) // + ); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); + }; + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public final OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.METER }; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + public AppManagerUtil getAppManagerUtil() { + return this.appManagerUtil; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.MULTIPLE; + } + + @Override + protected PqPlusMeter getApp() { + return this; + } + + public enum PqPlusModel implements TranslatableEnum { + UMD_96("Meter.PqPlus.UMD96", "App.Meter.PqPlus.UMD96"), // + UMD_97("Meter.PqPlus.UMD97", "App.Meter.PqPlus.UMD97"), // + ; + + private final String value; + private final String translation; + + private PqPlusModel(String value, String translation) { + this.value = value; + this.translation = translation; + } + + @Override + public String getTranslation(Language language) { + return translate(getTranslationBundle(language), this.translation); + } + + @Override + public String getValue() { + return this.value; + } + + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create()// + .setCanSee(Role.ADMIN)// + .setCanDelete(Role.ADMIN) // + .build(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/openemshardware/BeagleBoneBlack.java b/io.openems.edge.core/src/io/openems/edge/app/openemshardware/BeagleBoneBlack.java new file mode 100644 index 00000000000..19c4f155acd --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/openemshardware/BeagleBoneBlack.java @@ -0,0 +1,121 @@ +package io.openems.edge.app.openemshardware; + +import static io.openems.edge.app.common.props.CommonProps.alias; + +import java.util.Map; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.edge.app.openemshardware.BeagleBoneBlack.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; + +@Component(name = "App.OpenemsHardware.BeagleBoneBlack") +public class BeagleBoneBlack extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public static enum Property implements Type { + // Properties + ALIAS(alias()), // + ; + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + @Activate + public BeagleBoneBlack(// + @Reference ComponentManager componentManager, // + ComponentContext context, // + @Reference ConfigurationAdmin cm, // + @Reference ComponentUtil componentUtil // + ) { + super(componentManager, context, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + return AppConfiguration.empty(); + }; + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.OPENEMS_DEVICE_HARDWARE }; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + protected BeagleBoneBlack getApp() { + return this; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE_IN_CATEGORY; + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create() // + .setCanDelete(Role.ADMIN) // + .setCanSee(Role.ADMIN) // + .build(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/openemshardware/Compulab.java b/io.openems.edge.core/src/io/openems/edge/app/openemshardware/Compulab.java new file mode 100644 index 00000000000..dde30d24c6e --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/openemshardware/Compulab.java @@ -0,0 +1,121 @@ +package io.openems.edge.app.openemshardware; + +import static io.openems.edge.app.common.props.CommonProps.alias; + +import java.util.Map; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.edge.app.openemshardware.Compulab.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; + +@Component(name = "App.OpenemsHardware.Compulab") +public class Compulab extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public static enum Property implements Type { + // Properties + ALIAS(alias()), // + ; + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + @Activate + public Compulab(// + @Reference ComponentManager componentManager, // + ComponentContext context, // + @Reference ConfigurationAdmin cm, // + @Reference ComponentUtil componentUtil // + ) { + super(componentManager, context, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + return AppConfiguration.empty(); + }; + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.OPENEMS_DEVICE_HARDWARE }; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + protected Compulab getApp() { + return this; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE_IN_CATEGORY; + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create() // + .setCanDelete(Role.ADMIN) // + .setCanSee(Role.ADMIN) // + .build(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/openemshardware/TechbaseCm3.java b/io.openems.edge.core/src/io/openems/edge/app/openemshardware/TechbaseCm3.java new file mode 100644 index 00000000000..d89f0430bb1 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/openemshardware/TechbaseCm3.java @@ -0,0 +1,132 @@ +package io.openems.edge.app.openemshardware; + +import static io.openems.edge.app.common.props.CommonProps.alias; + +import java.util.Map; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.edge.app.openemshardware.TechbaseCm3.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; + +@Component(name = "App.OpenemsHardware.CM3") +public class TechbaseCm3 extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public static enum Property implements Type { + // Properties + ALIAS(alias()), // + ; + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + @Activate + public TechbaseCm3(// + @Reference ComponentManager componentManager, // + ComponentContext context, // + @Reference ConfigurationAdmin cm, // + @Reference ComponentUtil componentUtil // + ) { + super(componentManager, context, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + return AppConfiguration.create() // + .addDependencies(new DependencyDeclaration("IO_GPIO", // + DependencyDeclaration.CreatePolicy.IF_NOT_EXISTING, // + DependencyDeclaration.UpdatePolicy.NEVER, // + DependencyDeclaration.DeletePolicy.ALWAYS, // + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ONLY_UNCONFIGURED_PROPERTIES, // + DependencyDeclaration.DependencyDeletePolicy.NOT_ALLOWED, // + DependencyDeclaration.AppDependencyConfig.create() // + .setAppId("App.Hardware.IoGpio") // + .build())) + .build(); + }; + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.OPENEMS_DEVICE_HARDWARE }; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + protected TechbaseCm3 getApp() { + return this; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE_IN_CATEGORY; + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create() // + .setCanDelete(Role.ADMIN) // TODO maybe only system + .setCanSee(Role.ADMIN) // + .build(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/openemshardware/TechbaseCm4.java b/io.openems.edge.core/src/io/openems/edge/app/openemshardware/TechbaseCm4.java new file mode 100644 index 00000000000..f0a35bdc8fe --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/openemshardware/TechbaseCm4.java @@ -0,0 +1,121 @@ +package io.openems.edge.app.openemshardware; + +import static io.openems.edge.app.common.props.CommonProps.alias; + +import java.util.Map; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.edge.app.openemshardware.TechbaseCm4.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; + +@Component(name = "App.OpenemsHardware.CM4") +public class TechbaseCm4 extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public static enum Property implements Type { + // Properties + ALIAS(alias()), // + ; + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + @Activate + public TechbaseCm4(// + @Reference ComponentManager componentManager, // + ComponentContext context, // + @Reference ConfigurationAdmin cm, // + @Reference ComponentUtil componentUtil // + ) { + super(componentManager, context, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + return AppConfiguration.empty(); + }; + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.OPENEMS_DEVICE_HARDWARE }; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + protected TechbaseCm4 getApp() { + return this; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE_IN_CATEGORY; + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create() // + .setCanDelete(Role.ADMIN) // + .setCanSee(Role.ADMIN) // + .build(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/openemshardware/TechbaseCm4Max.java b/io.openems.edge.core/src/io/openems/edge/app/openemshardware/TechbaseCm4Max.java new file mode 100644 index 00000000000..a67442790bb --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/openemshardware/TechbaseCm4Max.java @@ -0,0 +1,121 @@ +package io.openems.edge.app.openemshardware; + +import static io.openems.edge.app.common.props.CommonProps.alias; + +import java.util.Map; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.edge.app.openemshardware.TechbaseCm4Max.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; + +@Component(name = "App.OpenemsHardware.CM4Max") +public class TechbaseCm4Max extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public static enum Property implements Type { + // Properties + ALIAS(alias()), // + ; + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + @Activate + public TechbaseCm4Max(// + @Reference ComponentManager componentManager, // + ComponentContext context, // + @Reference ConfigurationAdmin cm, // + @Reference ComponentUtil componentUtil // + ) { + super(componentManager, context, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + return AppConfiguration.empty(); + }; + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.OPENEMS_DEVICE_HARDWARE }; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + protected TechbaseCm4Max getApp() { + return this; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE_IN_CATEGORY; + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create() // + .setCanDelete(Role.ADMIN) // + .setCanSee(Role.ADMIN) // + .build(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/openemshardware/TechbaseCm4s.java b/io.openems.edge.core/src/io/openems/edge/app/openemshardware/TechbaseCm4s.java new file mode 100644 index 00000000000..8ce06dcb343 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/openemshardware/TechbaseCm4s.java @@ -0,0 +1,132 @@ +package io.openems.edge.app.openemshardware; + +import static io.openems.edge.app.common.props.CommonProps.alias; + +import java.util.Map; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.edge.app.openemshardware.TechbaseCm4s.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; + +@Component(name = "App.OpenemsHardware.CM4S") +public class TechbaseCm4s extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public static enum Property implements Type { + // Properties + ALIAS(alias()), // + ; + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + @Activate + public TechbaseCm4s(// + @Reference ComponentManager componentManager, // + ComponentContext context, // + @Reference ConfigurationAdmin cm, // + @Reference ComponentUtil componentUtil // + ) { + super(componentManager, context, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + return AppConfiguration.create() // + .addDependencies(new DependencyDeclaration("IO_GPIO", // + DependencyDeclaration.CreatePolicy.IF_NOT_EXISTING, // + DependencyDeclaration.UpdatePolicy.NEVER, // + DependencyDeclaration.DeletePolicy.ALWAYS, // + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ONLY_UNCONFIGURED_PROPERTIES, // + DependencyDeclaration.DependencyDeletePolicy.NOT_ALLOWED, // + DependencyDeclaration.AppDependencyConfig.create() // + .setAppId("App.Hardware.IoGpio") // + .build())) + .build(); + }; + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.OPENEMS_DEVICE_HARDWARE }; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + protected TechbaseCm4s getApp() { + return this; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE_IN_CATEGORY; + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create() // + .setCanDelete(Role.ADMIN) // + .setCanSee(Role.ADMIN) // + .build(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/openemshardware/TechbaseCm4sGen2.java b/io.openems.edge.core/src/io/openems/edge/app/openemshardware/TechbaseCm4sGen2.java new file mode 100644 index 00000000000..dd6b6df3c02 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/openemshardware/TechbaseCm4sGen2.java @@ -0,0 +1,137 @@ +package io.openems.edge.app.openemshardware; + +import static io.openems.edge.app.common.props.CommonProps.alias; + +import java.util.Map; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.oem.OpenemsEdgeOem; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.hardware.IoGpio; +import io.openems.edge.app.openemshardware.TechbaseCm4sGen2.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; + +@Component(name = "App.OpenemsHardware.CM4S.Gen2") +public class TechbaseCm4sGen2 extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public static enum Property implements Type { + // Properties + ALIAS(alias()), // + ; + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + @Activate + public TechbaseCm4sGen2(// + @Reference ComponentManager componentManager, // + ComponentContext context, // + @Reference ConfigurationAdmin cm, // + @Reference ComponentUtil componentUtil // + ) { + super(componentManager, context, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + return AppConfiguration.create() // + .addDependencies(new DependencyDeclaration("IO_GPIO", // + DependencyDeclaration.CreatePolicy.IF_NOT_EXISTING, // + DependencyDeclaration.UpdatePolicy.NEVER, // + DependencyDeclaration.DeletePolicy.ALWAYS, // + DependencyDeclaration.DependencyUpdatePolicy.ALLOW_ONLY_UNCONFIGURED_PROPERTIES, // + DependencyDeclaration.DependencyDeletePolicy.NOT_ALLOWED, // + DependencyDeclaration.AppDependencyConfig.create() // + .setAppId("App.Hardware.IoGpio") // + .setInitialProperties(JsonUtils.buildJsonObject() // + .addProperty(IoGpio.Property.IO_ID.name(), "io1") // + .build()) + .build())) + .build(); + }; + } + + @Override + public AppDescriptor getAppDescriptor(OpenemsEdgeOem oem) { + return AppDescriptor.create() // + .setWebsiteUrl(oem.getAppWebsiteUrl(this.getAppId())) // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.OPENEMS_DEVICE_HARDWARE }; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + protected TechbaseCm4sGen2 getApp() { + return this; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE_IN_CATEGORY; + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create() // + .setCanDelete(Role.ADMIN) // + .setCanSee(Role.ADMIN) // + .build(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java index 2a239b4579a..31200d39881 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java @@ -2,6 +2,7 @@ import static io.openems.edge.app.common.props.CommonProps.alias; import static io.openems.edge.app.common.props.CommonProps.defaultDef; +import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; import java.util.Map; @@ -168,7 +169,7 @@ public OpenemsAppCardinality getCardinality() { @Override protected ValidatorConfig.Builder getValidateBuilder() { return ValidatorConfig.create() // - .setCompatibleCheckableConfigs(checkHome()); + .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java index 417b2e8a67b..0c2f6d2f10d 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java @@ -1,5 +1,6 @@ package io.openems.edge.app.timeofusetariff; +import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; import java.util.Map; @@ -167,7 +168,7 @@ public OpenemsAppCardinality getCardinality() { @Override protected ValidatorConfig.Builder getValidateBuilder() { return ValidatorConfig.create() // - .setCompatibleCheckableConfigs(checkHome()); + .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/GroupeE.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/GroupeE.java index 1415ce3636b..b466c39ff10 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/GroupeE.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/GroupeE.java @@ -1,5 +1,6 @@ package io.openems.edge.app.timeofusetariff; +import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; import java.util.Map; @@ -150,7 +151,7 @@ public OpenemsAppCardinality getCardinality() { @Override protected ValidatorConfig.Builder getValidateBuilder() { return ValidatorConfig.create() // - .setCompatibleCheckableConfigs(checkHome()); + .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/RabotCharge.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/RabotCharge.java index c423c7bbcb9..de9a446a214 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/RabotCharge.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/RabotCharge.java @@ -1,6 +1,7 @@ package io.openems.edge.app.timeofusetariff; import static io.openems.edge.core.appmanager.formly.enums.InputType.PASSWORD; +import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; import java.util.Map; @@ -177,7 +178,7 @@ public OpenemsAppCardinality getCardinality() { @Override protected ValidatorConfig.Builder getValidateBuilder() { return ValidatorConfig.create() // - .setCompatibleCheckableConfigs(checkHome()); + .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StadtwerkHassfurt.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StadtwerkHassfurt.java index e013386d2dc..8bf099ba335 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StadtwerkHassfurt.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StadtwerkHassfurt.java @@ -1,6 +1,7 @@ package io.openems.edge.app.timeofusetariff; import static io.openems.edge.app.common.props.CommonProps.defaultDef; +import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; import java.util.Map; @@ -166,7 +167,7 @@ public OpenemsAppCardinality getCardinality() { @Override protected ValidatorConfig.Builder getValidateBuilder() { return ValidatorConfig.create() // - .setCompatibleCheckableConfigs(checkHome()); + .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java index 603fb81f5ba..fbbeb951333 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java @@ -1,5 +1,6 @@ package io.openems.edge.app.timeofusetariff; +import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; import java.util.Map; @@ -163,7 +164,7 @@ public OpenemsAppCardinality getCardinality() { @Override protected ValidatorConfig.Builder getValidateBuilder() { return ValidatorConfig.create() // - .setCompatibleCheckableConfigs(checkHome()); + .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java index 38b7b7b8b8b..3eeeb094bab 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java @@ -1,6 +1,7 @@ package io.openems.edge.app.timeofusetariff; import static io.openems.edge.core.appmanager.formly.enums.InputType.PASSWORD; +import static io.openems.edge.core.appmanager.validator.Checkables.checkCommercial92; import static io.openems.edge.core.appmanager.validator.Checkables.checkHome; import java.util.Map; @@ -196,7 +197,7 @@ public OpenemsAppCardinality getCardinality() { @Override protected ValidatorConfig.Builder getValidateBuilder() { return ValidatorConfig.create() // - .setCompatibleCheckableConfigs(checkHome()); + .setCompatibleCheckableConfigs(checkHome().or(checkCommercial92())); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppDef.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppDef.java index 806e0c79a63..777c07abda9 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppDef.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppDef.java @@ -32,7 +32,7 @@ * * @param the type of the app * @param the type of the property - * @param the type of the paramters + * @param the type of the parameters */ public class AppDef getHardwareMissmatch() { + return this.getAppsNotSyncedWithBackendChannel().value(); + } + + /** + * Gets the channel for {@link ChannelId#HARDWARE_MISSMATCH}. + * + * @return the Channel + */ + public default StateChannel getHardwareMissmatchChannel() { + return this.channel(ChannelId.HARDWARE_MISSMATCH); + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerImpl.java index e06ac96c3a3..25908265a55 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerImpl.java @@ -196,8 +196,8 @@ public final List getInstantiatedApps() { /** * formats the given apps into a JSON array string. * - * @param apps that should be formated - * @return formated apps string + * @param apps that should be formatted + * @return formatted apps string */ private static String getJsonAppsString(List apps) { return JsonUtils diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerUtil.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerUtil.java index 22649215c26..e1e7d3f1fe1 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerUtil.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerUtil.java @@ -51,6 +51,15 @@ public default List getInstantiatedAppsOf(String... appIds) .toList(); } + /** + * Gets the installed apps which match any of the provided + * {@link OpenemsAppCategory OpenemsAppCategories}. + * + * @param categories the {@link OpenemsAppCategory} to be contained by the app + * @return the found {@link OpenemsAppInstance OpenemsAppInstances} + */ + public List getInstantiatedAppsByCategories(OpenemsAppCategory... categories); + /** * Finds the {@link OpenemsApp} with the given id. * diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerUtilImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerUtilImpl.java index 188b47f064b..3337a884e12 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerUtilImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerUtilImpl.java @@ -6,6 +6,7 @@ import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -46,6 +47,22 @@ public Optional findInstanceById(UUID id) { .flatMap(t -> t.findInstanceById(id)); } + @Override + public List getInstantiatedAppsByCategories(OpenemsAppCategory... categories) { + return this.getInstantiatedApps().stream() // + .filter(t -> { + final var app = this.findAppById(t.appId).orElse(null); + + if (app == null) { + return false; + } + + return Stream.of(app.getCategories()) // + .anyMatch(c1 -> Stream.of(categories) // + .anyMatch(c2 -> c1 == c2)); + }).toList(); + } + @Override public AppConfiguration getAppConfiguration(ConfigurationTarget target, OpenemsApp app, String alias, JsonObject properties, Language language) throws OpenemsNamedException { diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsAppCategory.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsAppCategory.java index 3e287c25f9b..9f11f6825c7 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsAppCategory.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsAppCategory.java @@ -44,6 +44,11 @@ public enum OpenemsAppCategory { */ HARDWARE("hardware"), + /** + * The hardware on which the OpenEMS software runs. + */ + OPENEMS_DEVICE_HARDWARE("openemsDeviceHardware"), + /** * Peak-Shaving. */ diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/ResolveOpenemsHardware.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/ResolveOpenemsHardware.java new file mode 100644 index 00000000000..be8d1cee003 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/ResolveOpenemsHardware.java @@ -0,0 +1,189 @@ +package io.openems.edge.core.appmanager; + +import static java.util.Collections.emptyMap; +import static java.util.stream.Collectors.toMap; + +import java.io.File; +import java.nio.file.Files; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.component.annotations.ServiceScope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.openems.common.utils.JsonUtils; +import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance; + +@Component(immediate = true, scope = ServiceScope.SINGLETON) +public class ResolveOpenemsHardware implements Runnable { + + public static final String OPENEMS_HARDWARE_FILE_NAME = "hardware.conf"; + public static final String OPENEMS_HARDWARE_APP_KEY = "appId"; + + private final Logger log = LoggerFactory.getLogger(ResolveOpenemsHardware.class); + + private final ComponentContext context; + private final AppManagerImpl appManagerImpl; + private final AppManagerUtil appManagerUtil; + private final String requireApp; + private CompletableFuture task; + private final Executor delayedExecutor; + + /** + * Binds a {@link OpenemsApp}. + * + * @param app the {@link OpenemsApp} to bind + */ + @Reference(// + cardinality = ReferenceCardinality.MULTIPLE, // + policy = ReferencePolicy.DYNAMIC, // + policyOption = ReferencePolicyOption.GREEDY // + ) + public void bindApp(OpenemsApp app) { + this.trigger(); + } + + /** + * Unbinds a {@link OpenemsApp}. + * + * @param app the {@link OpenemsApp} to unbind + */ + public void unbindApp(OpenemsApp app) { + // empty + } + + public ResolveOpenemsHardware(// + ComponentContext context, // + AppManager appManagerImpl, // + AppManagerUtil appManagerUtil, // + Executor delayedExecutor // + ) { + super(); + this.context = context; + this.appManagerImpl = (AppManagerImpl) appManagerImpl; + this.appManagerUtil = appManagerUtil; + this.delayedExecutor = delayedExecutor; + + final var hardwareProperties = this.getHardwareProperties(); + this.requireApp = hardwareProperties.get(OPENEMS_HARDWARE_APP_KEY); + + this.trigger(); + } + + @Activate + public ResolveOpenemsHardware(// + ComponentContext context, // + @Reference AppManager appManagerImpl, // + @Reference AppManagerUtil appManagerUtil // + ) { + this(context, appManagerImpl, appManagerUtil, CompletableFuture.delayedExecutor(5, TimeUnit.SECONDS)); + } + + @Deactivate + private void deactivate() { + final var task = this.task; + if (task != null) { + task.cancel(false); + } + } + + private synchronized void trigger() { + if (this.requireApp == null) { + this.task = CompletableFuture.runAsync(this.context.getComponentInstance()::dispose); + return; + } + + final var activeTask = this.task; + if (activeTask != null) { + activeTask.cancel(false); + } + + // waits 5 seconds until every app is activated + this.task = CompletableFuture.runAsync(() -> { + try { + this.run(); + } finally { + this.context.getComponentInstance().dispose(); + } + }, this.delayedExecutor); + } + + @Override + public void run() { + // currently its ignored if the app of an installed hardware app is not yet + // active or available after a 5 seconds delay from the last activated app and + // would therefore not be returned by the following method which could result in + // 2 hardware apps being installed + final var hardwareApps = this.appManagerUtil + .getInstantiatedAppsByCategories(OpenemsAppCategory.OPENEMS_DEVICE_HARDWARE); + + if (hardwareApps.size() >= 1) { + // validate + if (hardwareApps.size() > 1) { + // more than 1 installed => impossible + this.appManagerImpl._setHardwareMissmatch(true); + return; + } + + final var installedHardwareApp = hardwareApps.get(0); + if (installedHardwareApp.appId.equals(this.requireApp)) { + // installed hardware app matches the on in the properties file + this.appManagerImpl._setHardwareMissmatch(false); + return; + } + + this.appManagerImpl._setHardwareMissmatch(true); + return; + } + + this.log.trace("Try to install '" + this.requireApp + "'"); + try { + this.appManagerImpl.handleAddAppInstanceRequest(null, + new AddAppInstance.Request(this.requireApp, null, null, JsonUtils.buildJsonObject() // + .build()), + true); + + this.log.trace("Installed '" + this.requireApp + "' successfully"); + this.appManagerImpl._setHardwareMissmatch(false); + } catch (Exception e) { + this.log.error("Installation of '" + this.requireApp + "' failed", e); + this.appManagerImpl._setHardwareMissmatch(true); + } + + } + + private Map getHardwareProperties() { + final var configDir = System.getProperty("felix.cm.dir"); + if (configDir == null) { + return emptyMap(); + } + final var hardwareFile = new File(configDir, OPENEMS_HARDWARE_FILE_NAME); + + if (!hardwareFile.exists()) { + return emptyMap(); + } + + try { + return Files.lines(hardwareFile.toPath()) // + .map(t -> t.split("=", 2)) // + .filter(t -> t.length == 2) // + .collect(toMap(t -> t[0], t -> t[1])); + + } catch (Exception e) { + this.log.error("Unable to read hardware info file", e); + return emptyMap(); + } + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelperImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelperImpl.java index b6b13ef0916..7ce7533c0f2 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelperImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelperImpl.java @@ -30,6 +30,7 @@ import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.component.annotations.ServiceScope; +import org.osgi.service.condition.Condition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,6 +58,11 @@ import io.openems.edge.core.appmanager.TranslationUtil; import io.openems.edge.core.appmanager.dependency.DependencyDeclaration.AppDependencyConfig; import io.openems.edge.core.appmanager.dependency.aggregatetask.AggregateTask; +import io.openems.edge.core.appmanager.dependency.aggregatetask.ComponentAggregateTask; +import io.openems.edge.core.appmanager.dependency.aggregatetask.PersistencePredictorAggregateTask; +import io.openems.edge.core.appmanager.dependency.aggregatetask.SchedulerAggregateTask; +import io.openems.edge.core.appmanager.dependency.aggregatetask.SchedulerByCentralOrderAggregateTask; +import io.openems.edge.core.appmanager.dependency.aggregatetask.StaticIpAggregateTask; @Component(// immediate = true, // @@ -66,6 +72,23 @@ public class AppManagerAppHelperImpl implements AppManagerAppHelper { private final Logger log = LoggerFactory.getLogger(this.getClass()); + @Component(service = { StartTarget.class }) + public static class StartTarget implements Condition { + @Reference + private ComponentAggregateTask componentAggregateTask; + @Reference + private PersistencePredictorAggregateTask persistencePredictorAggregateTask; + @Reference + private SchedulerAggregateTask schedulerAggregateTask; + @Reference + private SchedulerByCentralOrderAggregateTask schedulerByCentralOrderAggregateTask; + @Reference + private StaticIpAggregateTask staticIpAggregateTask; + } + + @Reference + private StartTarget startCondition; + @Reference(// policy = ReferencePolicy.DYNAMIC, // policyOption = ReferencePolicyOption.GREEDY, // diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerByCentralOrderAggregateTaskImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerByCentralOrderAggregateTaskImpl.java index 639fad889a4..c7b69815ce1 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerByCentralOrderAggregateTaskImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerByCentralOrderAggregateTaskImpl.java @@ -62,6 +62,7 @@ public ProductionSchedulerOrderDefinition() { .thenByFactoryId("Controller.Ess.FixActivePower") // .thenByFactoryId("Controller.Ess.FixStateOfCharge")// .thenByFactoryId("Controller.Ess.EmergencyCapacityReserve") // + .thenByFactoryId("Controller.Ess.Limiter14a") // .thenBy(new SchedulerOrderDefinition() // .filterByFactoryId("Controller.Api.ModbusTcp.ReadWrite") // .thenByCreatedAppId("App.Ess.GeneratingPlantController") // diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties index 78905f75ed7..b0e860ca53e 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties @@ -11,6 +11,7 @@ meter = Erzeugungs- und Verbrauchszähler peakShaving = Lastspitzenkappung und atypische Netznutzung api = Schnittstellen ess = Speichersystemsteuerung +openemsDeviceHardware = OpenEMS Geräte Hardware timedata = Timedata test = Test @@ -172,6 +173,30 @@ App.Hardware.KMtronic8Channel.Name = FEMS Relaisboard 8-Kanal TCP App.Hardware.KMtronic8Channel.Name.short = FEMS Relaisboard 8-Kanal TCP App.Hardware.KMtronic8Channel.ip.description = Die IP-Adresse des Relaisboards App.Hardware.KMtronic8Channel.installationHint = Hinweis: Stellen Sie vor der Installation sicher, dass das Relaisboard elektrisch als auch netzwerkseitig korrekt angeschlossen wurde. Andernfalls können Fehlermeldungen im Online-Monitoring auftreten. +App.Hardware.IoGpio.Name = IO GPIO +App.Hardware.IoGpio.Name.short = IO GPIO + +# Openems device hardware +App.OpenemsHardware.BeagleBoneBlack.Name = BeagleBoneBlack +App.OpenemsHardware.BeagleBoneBlack.Name.short = BeagleBoneBlack + +App.OpenemsHardware.Compulab.Name = Compulab +App.OpenemsHardware.Compulab.Name.short = Compulab + +App.OpenemsHardware.CM3.Name = Techbase Compute Module 3 +App.OpenemsHardware.CM3.Name.short = Techbase CM3 + +App.OpenemsHardware.CM4.Name = Techbase Compute Module 4 +App.OpenemsHardware.CM4.Name.short = Techbase CM4 + +App.OpenemsHardware.CM4S.Name = Techbase Compute Module 4S +App.OpenemsHardware.CM4S.Name.short = Techbase CM4S + +App.OpenemsHardware.CM4S.Gen2.Name = Techbase Compute Module 4S Gen 2 +App.OpenemsHardware.CM4S.Gen2.Name.short = Techbase CM4S Gen 2 + +App.OpenemsHardware.CM4Max.Name = Techbase Compute Module 4 Max +App.OpenemsHardware.CM4Max.Name.short = Techbase CM4 Max # Heat App.Heat.CHP.Name = Blockheizkraftwerk (BHKW) @@ -219,12 +244,14 @@ App.IntegratedSystem.gridMeterType.option.commercialMeter = Home 3-Phasensensor App.IntegratedSystem.ctRatioFirst.label = Wandler-Primärstrom (200A - 5000A/5A) App.IntegratedSystem.shadowManagementDisabled.label = Schattenmanagement deaktivieren App.IntegratedSystem.shadowManagementDisabled.description = Nur wenn Optimierer verbaut sind, muss das Schattenmanagement deaktiviert werden +App.IntegratedSystem.hasEssLimiter14a.label = Hat Limitierer für §14a App.IntegratedSystem.modbus0.alias = Kommunikation mit der Batterie App.IntegratedSystem.modbus0N.alias = Kommunikation mit den Batterien App.IntegratedSystem.modbus1.alias = Kommunikation mit dem Batterie-Wechselrichter App.IntegratedSystem.modbus1N.alias = Kommunikation mit dem Batterie-Wechselrichter {0} App.IntegratedSystem.modbus2.alias = externe RS485 Schnittstelle +App.IntegratedSystem.modbusToGridMeter.alias = Kommunikation mit dem Netzzähler App.IntegratedSystem.io0.alias = EMS Box Relais App.IntegratedSystem.battery0.alias = Batterie App.IntegratedSystem.batteryN.alias = Batterie {0} @@ -282,6 +309,19 @@ App.FENECON.Home.30.Name.short = FENECON Home 30 App.FENECON.Home.20.Name = FENECON Home 20 App.FENECON.Home.20.Name.short = FENECON Home 20 +App.FENECON.Commercial.92.Name = FENECON Commercial +App.FENECON.Commercial.92.Name.short = FENECON Commercial +App.FENECON.Industrial.L.io0 = Relais +App.FENECON.Industrial.L.Name = FENECON Industrial L +App.FENECON.Industrial.L.Name.short = FENECON Industrial L +App.FENECON.Industrial.L.modbus0.alias = Kommunikation mit dem Klimagerät +App.FENECON.Industrial.L.essN.alias = Batteriespeicher {0} + +App.FENECON.Industrial.L.ILK710.Name = FENECON Industrial L ILK710 +App.FENECON.Industrial.L.ILK710.Name.short = ILK710 +App.FENECON.Industrial.L.ILK710.batteryProtectionType.label = Battery Protection Typ +App.FENECON.Industrial.L.ILK710.batteryFirmwareVersion.label = Battery Firmware Version + App.FENECON.Industrial.S.io0 = Relais App.FENECON.Industrial.S.ess0.alias = Batteriespeicher App.FENECON.Industrial.S.essN.alias = Batteriespeicher {0} @@ -298,6 +338,7 @@ App.FENECON.Industrial.S.ISK011.Name = FENECON Industrial S ISK011 App.FENECON.Industrial.S.ISK011.Name.short = ISK011 # Meter +App.Meter.alias = Kommunikation mit dem Zähler App.Meter.mountType.label = Verwendungsart App.Meter.modbusUnitId.description = Modbus Unit-ID des Zählers App.Meter.ip.description = IP-Adresse des Zählers @@ -311,6 +352,11 @@ App.Meter.CarloGavazzi.Name.short = CARLO GAVAZZI App.Meter.Janitza.Name = Janitza Zähler App.Meter.Janitza.Name.short = Janitza App.Meter.Janitza.productModel = Gerätemodell +App.Meter.PqPlus.UMD96 = PQ-Plus Zähler UMD96 +App.Meter.PqPlus.UMD97 = PQ-Plus Zähler UMD97 +App.Meter.PqPlus.Name = PQ-Plus Zähler +App.Meter.PqPlus.Name.short = PQ-Plus +App.Meter.PqPlus.productModel = Gerätemodell App.Meter.Socomec.Name = SOCOMEC Zähler App.Meter.Socomec.Name.short = SOCOMEC App.Meter.Kdk.Name = KDK Zähler @@ -330,7 +376,8 @@ App.Meter.Discovergy.fullSerialNumber.label = Discovergy Komplette Seriennummer App.Meter.Discovergy.meterId.label = Discovergy MeterId App.Meter.Discovergy.meterId.description = Interne Zähler Id. Dieser is ein Hex String mit Länge 32. App.Meter.Discovergy.serialType.label = Typ der Seriennummer - +App.Meter.PhoenixContact.Name = PhoenixContact Zähler +App.Meter.PhoenixContact.Name.short = PhoenixContact # PeakShaving App.PeakShaving.power.label = Maximale Netzbezugsleistung @@ -443,6 +490,13 @@ App.Ess.FixStateOfCharge.targetSoc.label = Ziel-SoC App.Ess.FixStateOfCharge.targetTime.label = Zielzeit App.Ess.FixStateOfCharge.targetTimeBuffer.label = Zielzeitpuffer App.Ess.FixStateOfCharge.selfTermination.label = Beendet sich selbst am Ende -App.Ess.FixStateOfCharge.terminationBuffer.label = Zeitpuffer zum Beenden +App.Ess.FixStateOfCharge.terminationBuffer.label = Zeitpuffer zum Beenden App.Ess.FixStateOfCharge.conditionalTermination.label = Beendet sich selbst nach separater Bedingung -App.Ess.FixStateOfCharge.isRunning.label = Kontroller aktiv? \ No newline at end of file +App.Ess.FixStateOfCharge.isRunning.label = Kontroller aktiv? + +App.Ess.Limiter14a.Name = ESS Limiter für §14a +App.Ess.Limiter14a.Name.short = ESS Limiter §14a + +AppManager.DefectiveApp = Defekte App erkannt +AppManager.WrongAppConfiguration = App-Manager Konfiguration ist falsch +AppManager.AppsNotSynced = Die aktuell installierten Apps sind nicht dieselben wie im Backend hinterlegt diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties index 3e1bbf49ec2..460116c4f56 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties @@ -11,6 +11,7 @@ meter = Production and consumption meter peakShaving = Peak shaving and atypical grid usage api = Interfaces ess = Energy Storage controller +openemsDeviceHardware = OpenEMS Device Hardware timedata = Timedata test = Test @@ -172,6 +173,30 @@ App.Hardware.KMtronic8Channel.Name = FEMS Relay board 8-channel TCP App.Hardware.KMtronic8Channel.Name.short = FEMS Relay board 8-channel TCP App.Hardware.KMtronic8Channel.ip.description = The IP address of the relay board App.Hardware.KMtronic8Channel.installationHint = Note: Before installing the relay board please make sure that it is connected correctly both electrically and to the network. Otherwise error messages in the Online-Monitoring may occur. +App.Hardware.IoGpio.Name = IO GPIO +App.Hardware.IoGpio.Name.short = IO GPIO + +# Openems device hardware +App.OpenemsHardware.BeagleBoneBlack.Name = BeagleBoneBlack +App.OpenemsHardware.BeagleBoneBlack.Name.short = BeagleBoneBlack + +App.OpenemsHardware.Compulab.Name = Compulab +App.OpenemsHardware.Compulab.Name.short = Compulab + +App.OpenemsHardware.CM3.Name = Techbase Compute Module 3 +App.OpenemsHardware.CM3.Name.short = Techbase CM3 + +App.OpenemsHardware.CM4.Name = Techbase Compute Module 4 +App.OpenemsHardware.CM4.Name.short = Techbase CM4 + +App.OpenemsHardware.CM4S.Name = Techbase Compute Module 4S +App.OpenemsHardware.CM4S.Name.short = Techbase CM4S + +App.OpenemsHardware.CM4S.Gen2.Name = Techbase Compute Module 4S Gen 2 +App.OpenemsHardware.CM4S.Gen2.Name.short = Techbase CM4S Gen 2 + +App.OpenemsHardware.CM4Max.Name = Techbase Compute Module 4 Max +App.OpenemsHardware.CM4Max.Name.short = Techbase CM4 Max # Heat App.Heat.CHP.Name = Combined heat and power plant (CHP) @@ -219,12 +244,14 @@ App.IntegratedSystem.gridMeterType.option.commercialMeter = Home 3-phase sensor App.IntegratedSystem.ctRatioFirst.label = CT-Ratio (200A - 5000A/5A) App.IntegratedSystem.shadowManagementDisabled.label = Deactivate shadow management App.IntegratedSystem.shadowManagementDisabled.description = Only if optimisers are installed, shadow management must be deactivated +App.IntegratedSystem.hasEssLimiter14a.label = Has limiter for §14a App.IntegratedSystem.modbus0.alias = Communication with the battery App.IntegratedSystem.modbus0N.alias = Communication with the batteries App.IntegratedSystem.modbus1.alias = Communication with the battery inverter App.IntegratedSystem.modbus1N.alias = Communication with the battery inverter {0} App.IntegratedSystem.modbus2.alias = external RS485 interface +App.IntegratedSystem.modbusToGridMeter.alias = Communication with the Grid-Meter App.IntegratedSystem.io0.alias = EMS Box Relay App.IntegratedSystem.battery0.alias = battery App.IntegratedSystem.batteryN.alias = battery {0} @@ -282,6 +309,19 @@ App.FENECON.Home.30.Name.short = FENECON Home 30 App.FENECON.Home.20.Name = FENECON Home 20 App.FENECON.Home.20.Name.short = FENECON Home 20 +App.FENECON.Commercial.92.Name = FENECON Commercial +App.FENECON.Commercial.92.Name.short = FENECON Commercial +App.FENECON.Industrial.L.io0 = Relay +App.FENECON.Industrial.L.Name = FENECON Industrial L +App.FENECON.Industrial.L.Name.short = FENECON Industrial L +App.FENECON.Industrial.L.modbus0.alias = Communication with Cooling Unit +App.FENECON.Industrial.L.essN.alias = Battery Storage {0} + +App.FENECON.Industrial.L.ILK710.Name = FENECON Industrial L ILK710 +App.FENECON.Industrial.L.ILK710.Name.short = ILK710 +App.FENECON.Industrial.L.ILK710.batteryProtectionType.label = Battery Protection Typ +App.FENECON.Industrial.L.ILK710.batteryFirmwareVersion.label = Battery Firmware Version + App.FENECON.Industrial.S.io0 = Relay App.FENECON.Industrial.S.ess0.alias = Battery Storage App.FENECON.Industrial.S.essN.alias = Battery Storage {0} @@ -298,6 +338,7 @@ App.FENECON.Industrial.S.ISK011.Name = FENECON Industrial S ISK011 App.FENECON.Industrial.S.ISK011.Name.short = ISK011 # Meter +App.Meter.alias = Communication with the Meter App.Meter.mountType.label = Mount Type App.Meter.modbusUnitId.description = Modbus Unit-ID of the Meter App.Meter.ip.description = IP address of the Meter @@ -311,6 +352,11 @@ App.Meter.CarloGavazzi.Name.short = CARLO GAVAZZI App.Meter.Janitza.Name = Janitza meter App.Meter.Janitza.Name.short = Janitza App.Meter.Janitza.productModel = Device model +App.Meter.PqPlus.UMD96 = PQ-Plus meter UMD96 +App.Meter.PqPlus.UMD97 = PQ-Plus meter UMD97 +App.Meter.PqPlus.Name = PQ-Plus meter +App.Meter.PqPlus.Name.short = PQ-Plus +App.Meter.PqPlus.productModel = Device model App.Meter.Socomec.Name = SOCOMEC meter App.Meter.Socomec.Name.short = SOCOMEC App.Meter.Kdk.Name = KDK meter @@ -330,6 +376,8 @@ App.Meter.Discovergy.fullSerialNumber.label = Discovergy Full Serial-Number App.Meter.Discovergy.meterId.label = Discovergy MeterId App.Meter.Discovergy.meterId.description = Internal MeterId. This is a hex string with length 32. App.Meter.Discovergy.serialType.label = Type of the serial number +App.Meter.PhoenixContact.Name = PhoenixContact Meter +App.Meter.PhoenixContact.Name.short = PhoenixContact # PeakShaving App.PeakShaving.power.label = Peak-Shaving power @@ -351,6 +399,7 @@ App.PeakShaving.PhaseAccuratePeakShaving.Name.short = Phase accurate peak shavin # PV inverter App.PvInverter.ip.description = IP address of the PV inverter. App.PvInverter.port.description = Port of the PV inverter. +App.PvInverter.modbusUnitId.description = Modbus Unit-ID of the PV inverter. App.PvInverter.phase.label = Phase App.PvInverter.phase.description = Connected phase(s) of the inverter @@ -366,7 +415,7 @@ App.PvInverter.Sma.modbusUnitId.description = The Unit-ID of the Modbus device. App.PvInverter.SolarEdge.Name = SolarEdge PV inverter App.PvInverter.SolarEdge.Name.short = SolarEdge -# Time of use Tarif +# Time of use Tariff App.TimeOfUseTariff.Awattar.Name = Time-of-Use Tariff (Awattar HOURLY) App.TimeOfUseTariff.Awattar.Name.short = Awattar HOURLY App.TimeOfUseTariff.Awattar.zone.label = Zone @@ -405,7 +454,7 @@ App.TimeOfUseTariff.Tibber.accessToken.label = Token App.TimeOfUseTariff.Tibber.accessToken.description = To link to your Tibber account you need a personal access token. You can create this under "developer.tibber.com/settings/access-token". App.TimeOfUseTariff.Tibber.filterForHome.label = Filter for Home App.TimeOfUseTariff.Tibber.filterForHome.description = For multiple 'Homes', add either an ID (format UUID) or 'appNickname' for unambiguous identification -App.TimeOfUseTariff.Tibber.multipleHomesCheck.label = Do you have more than one contract connected toyour Tibber account? +App.TimeOfUseTariff.Tibber.multipleHomesCheck.label = Do you have more than one contract connected to your Tibber account? # PvSelfConsumption App.PvSelfConsumption.GridOptimizedCharge.Name = Grid-optimized charge @@ -441,6 +490,13 @@ App.Ess.FixStateOfCharge.targetSoc.label = Target SoC App.Ess.FixStateOfCharge.targetTime.label = Target time [YYYY-MM-DDTHH:mm:ssTZD eg. 2023-12-15T13:47:20+01:00] App.Ess.FixStateOfCharge.targetTimeBuffer.label = Target time buffer App.Ess.FixStateOfCharge.selfTermination.label = Terminates itself at the end -App.Ess.FixStateOfCharge.terminationBuffer.label = Terminate time buffer in min -App.Ess.FixStateOfCharge.conditionalTermination.label = Terminates itself after separate conditon +App.Ess.FixStateOfCharge.terminationBuffer.label = Terminate time buffer in min +App.Ess.FixStateOfCharge.conditionalTermination.label = Terminates itself after separate condition App.Ess.FixStateOfCharge.isRunning.label = Controller active? + +App.Ess.Limiter14a.Name = ESS limiter for §14a +App.Ess.Limiter14a.Name.short = ESS limiter §14a + +AppManager.DefectiveApp = Defective App detected +AppManager.WrongAppConfiguration = App-Manager configuration is wrong +AppManager.AppsNotSynced = The currently installed apps are not the same as logged in the backend diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckCommercial92.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckCommercial92.java new file mode 100644 index 00000000000..2db34ca6fc4 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckCommercial92.java @@ -0,0 +1,51 @@ +package io.openems.edge.core.appmanager.validator; + +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ServiceScope; + +import io.openems.common.OpenemsConstants; +import io.openems.common.session.Language; + +@Component(// + name = CheckCommercial92.COMPONENT_NAME, // + scope = ServiceScope.PROTOTYPE // +) +public class CheckCommercial92 extends AbstractCheckable implements Checkable { + + public static final String COMPONENT_NAME = "Validator.Checkable.CheckCommercial92"; + + private final Checkable checkAppsNotInstalled; + + @Activate + public CheckCommercial92(// + ComponentContext componentContext, // + @Reference(target = "(" + OpenemsConstants.PROPERTY_OSGI_COMPONENT_NAME + "=" + + CheckAppsNotInstalled.COMPONENT_NAME + ")") Checkable checkAppsNotInstalled // + ) { + super(componentContext); + this.checkAppsNotInstalled = checkAppsNotInstalled; + } + + @Override + public boolean check() { + this.checkAppsNotInstalled.setProperties(Checkables.checkAppsNotInstalled(// + "App.FENECON.Commercial.92" // + ).properties()); + + return !this.checkAppsNotInstalled.check(); + } + + @Override + public String getErrorMessage(Language language) { + return AbstractCheckable.getTranslation(language, "Validator.Checkable.CheckCommercial92.Message"); + } + + @Override + public String getInvertedErrorMessage(Language language) { + return AbstractCheckable.getTranslation(language, "Validator.Checkable.CheckCommercial92.Message.Inverted"); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHome.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHome.java index 8d58d508b08..45dcf2223e4 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHome.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHome.java @@ -8,7 +8,6 @@ import io.openems.common.OpenemsConstants; import io.openems.common.session.Language; -import io.openems.edge.common.component.ComponentManager; @Component(// name = CheckHome.COMPONENT_NAME, // @@ -18,37 +17,27 @@ public class CheckHome extends AbstractCheckable implements Checkable { public static final String COMPONENT_NAME = "Validator.Checkable.CheckHome"; - private final ComponentManager componentManager; private final Checkable checkAppsNotInstalled; @Activate public CheckHome(// - @Reference ComponentManager componentManager, // ComponentContext componentContext, // @Reference(target = "(" + OpenemsConstants.PROPERTY_OSGI_COMPONENT_NAME + "=" + CheckAppsNotInstalled.COMPONENT_NAME + ")") Checkable checkAppsNotInstalled // ) { super(componentContext); - this.componentManager = componentManager; this.checkAppsNotInstalled = checkAppsNotInstalled; } @Override public boolean check() { - var batteries = this.componentManager.getEdgeConfig().getComponentsByFactory("Battery.Fenecon.Home"); this.checkAppsNotInstalled.setProperties(Checkables.checkAppsNotInstalled(// "App.FENECON.Home", // "App.FENECON.Home.20", // "App.FENECON.Home.30" // ).properties()); - // TODO remove check for batteries - // not every home has the home app installed but if a batterie of an home is - // installed its probably a home and so the app can be used. - // later there should only be checked if the home app is installed because if - // the configuration is wrong there may be no home battery installed and so the - // app wouldn't be available even though it is a home - return !batteries.isEmpty() || !this.checkAppsNotInstalled.check(); + return !this.checkAppsNotInstalled.check(); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckOr.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckOr.java new file mode 100644 index 00000000000..eb277678027 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckOr.java @@ -0,0 +1,102 @@ +package io.openems.edge.core.appmanager.validator; + +import java.util.Map; +import java.util.Objects; + +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ServiceScope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.openems.common.session.Language; +import io.openems.edge.core.appmanager.validator.CheckableFactory.ClosableCheckable; +import io.openems.edge.core.appmanager.validator.ValidatorConfig.CheckableConfig; + +@Component(// + name = CheckOr.COMPONENT_NAME, // + scope = ServiceScope.PROTOTYPE // +) +public class CheckOr extends AbstractCheckable implements Checkable { + + public static final String COMPONENT_NAME = "Validator.Checkable.CheckOr"; + + private final Logger log = LoggerFactory.getLogger(CheckOr.class); + + private final CheckableFactory checkableFactory; + + private CheckableConfig check1Config; + private ClosableCheckable check1; + private CheckableConfig check2Config; + private ClosableCheckable check2; + + @Activate + public CheckOr(// + ComponentContext componentContext, // + @Reference CheckableFactory checkableFactory // + ) { + super(componentContext); + this.checkableFactory = checkableFactory; + } + + @Deactivate + private void deactivate() { + try { + this.check1.close(); + } catch (Exception e) { + this.log.error("Unable to close checkable " + this.check1Config.checkableComponentName(), e); + } + try { + this.check2.close(); + } catch (Exception e) { + this.log.error("Unable to close checkable " + this.check2Config.checkableComponentName(), e); + } + } + + @Override + public void setProperties(Map properties) { + this.check1Config = Objects.requireNonNull((CheckableConfig) properties.get("check1"), + "First check must not be null"); + this.check2Config = Objects.requireNonNull((CheckableConfig) properties.get("check2"), + "Second check must not be null"); + } + + @Override + public boolean check() { + this.check1 = this.checkableFactory.useCheckable(this.check1Config.checkableComponentName()); + this.check1.setProperties(this.check1Config.properties()); + this.check2 = this.checkableFactory.useCheckable(this.check2Config.checkableComponentName()); + this.check2.setProperties(this.check2Config.properties()); + + final var check1Failed = this.check1.check() == this.check1Config.invertResult(); + final var check2Failed = this.check2.check() == this.check2Config.invertResult(); + return !(check1Failed && check2Failed); + } + + @Override + public String getErrorMessage(Language language) { + return AbstractCheckable.getTranslation(language, "Validator.Checkable.CheckOr.Message", + this.getCheck1ErrorMessage(language), this.getCheck2ErrorMessage(language)); + } + + @Override + public String getInvertedErrorMessage(Language language) { + throw new UnsupportedOperationException(); + } + + private String getCheck1ErrorMessage(Language language) { + return this.check1Config.invertResult() // + ? this.check1.getInvertedErrorMessage(language) + : this.check1.getErrorMessage(language); + } + + private String getCheck2ErrorMessage(Language language) { + return this.check2Config.invertResult() // + ? this.check2.getInvertedErrorMessage(language) + : this.check2.getErrorMessage(language); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckableFactory.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckableFactory.java new file mode 100644 index 00000000000..d96603c3d3f --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckableFactory.java @@ -0,0 +1,130 @@ +package io.openems.edge.core.appmanager.validator; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +import org.osgi.service.component.ComponentServiceObjects; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.component.annotations.ReferenceScope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.openems.common.OpenemsConstants; +import io.openems.common.session.Language; + +@Component(// + service = CheckableFactory.class // +) +public class CheckableFactory { + + private final Logger log = LoggerFactory.getLogger(CheckableFactory.class); + + private final Map> checkableFactories = new HashMap<>(); + + /** + * Binds a {@link Checkable} {@link ComponentServiceObjects}. + * + * @param cso the cso to bind + */ + @Reference(// + cardinality = ReferenceCardinality.MULTIPLE, // + policy = ReferencePolicy.DYNAMIC, // + policyOption = ReferencePolicyOption.GREEDY, // + // requires prototype for thread safety + scope = ReferenceScope.PROTOTYPE_REQUIRED // + ) + public void bindCso(ComponentServiceObjects cso) { + var sr = cso.getServiceReference(); + var srName = (String) sr.getProperty(OpenemsConstants.PROPERTY_OSGI_COMPONENT_NAME); + this.checkableFactories.put(srName, cso); + } + + /** + * Unbinds a {@link Checkable} {@link ComponentServiceObjects}. + * + * @param cso the cso to unbind + */ + public void unbindCso(ComponentServiceObjects cso) { + var sr = cso.getServiceReference(); + var srName = (String) sr.getProperty(OpenemsConstants.PROPERTY_OSGI_COMPONENT_NAME); + this.checkableFactories.remove(srName); + } + + @Activate + public CheckableFactory() { + } + + /** + * Gets a {@link Checkable} which component name matches the provided name. + * + * @param checkableComponentName the component name to search for + * @return a found {@link Checkable} service or null if not found + */ + public ClosableCheckable useCheckable(String checkableComponentName) { + final var cso = this.checkableFactories.get(checkableComponentName); + if (cso == null) { + this.log.warn("Unable to find checkable with name '" + checkableComponentName + "'."); + return null; + } + + final var service = cso.getService(); + if (service == null) { + this.log.warn("Unable to create checkable with name '" + checkableComponentName + "'."); + return null; + } + + return new ClosableCheckable(service, t -> { + cso.ungetService(service); + }); + } + + public static class ClosableCheckable implements Checkable, AutoCloseable { + + private final Checkable checkable; + private final Consumer onClose; + + private ClosableCheckable(Checkable checkable, Consumer onClose) { + super(); + this.checkable = checkable; + this.onClose = onClose; + } + + @Override + public String getComponentName() { + return this.checkable.getComponentName(); + } + + @Override + public void setProperties(Map properties) { + this.checkable.setProperties(properties); + } + + @Override + public boolean check() { + return this.checkable.check(); + } + + @Override + public String getErrorMessage(Language language) { + return this.checkable.getErrorMessage(language); + } + + @Override + public String getInvertedErrorMessage(Language language) { + return this.checkable.getInvertedErrorMessage(language); + } + + @Override + public void close() throws Exception { + this.onClose.accept(this.checkable); + } + + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkables.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkables.java index 65b03fa36c4..32783117bed 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkables.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkables.java @@ -19,6 +19,32 @@ public static CheckableConfig checkHome() { return empty(CheckHome.COMPONENT_NAME); } + /** + * Creates a {@link CheckableConfig} which checks if the installed system is a + * Home. + * + * @return the {@link CheckableConfig} + */ + public static CheckableConfig checkCommercial92() { + return empty(CheckCommercial92.COMPONENT_NAME); + } + + /** + * Creates a {@link CheckableConfig} which checks if atleast one of the checks + * are successful. + * + * @param check1 the first check + * @param check2 the second check + * @return the {@link CheckableConfig} + */ + public static CheckableConfig checkOr(CheckableConfig check1, CheckableConfig check2) { + return new ValidatorConfig.CheckableConfig(CheckOr.COMPONENT_NAME, + new ValidatorConfig.MapBuilder<>(new TreeMap()) // + .put("check1", check1) // + .put("check2", check2) // + .build()); + } + /** * Creates a {@link CheckableConfig} which checks if the relay with the given * name has at least the given amount of ports available. diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorConfig.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorConfig.java index 8c7c05745a0..9598943b060 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorConfig.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorConfig.java @@ -74,6 +74,17 @@ public CheckableConfig invert() { ); } + /** + * Creates a {@link CheckableConfig} which checks if the current check is + * successful or the other check. + * + * @param other the other check + * @return the {@link CheckableConfig} + */ + public CheckableConfig or(CheckableConfig other) { + return Checkables.checkOr(this, other); + } + @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorImpl.java index d8868d85b75..77b03ae4253 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorImpl.java @@ -5,70 +5,44 @@ import java.util.ArrayList; import java.util.List; -import org.osgi.service.component.ComponentServiceObjects; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; -import org.osgi.service.component.annotations.ReferenceCardinality; -import org.osgi.service.component.annotations.ReferencePolicy; -import org.osgi.service.component.annotations.ReferencePolicyOption; -import org.osgi.service.component.annotations.ReferenceScope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.openems.common.OpenemsConstants; import io.openems.common.session.Language; import io.openems.edge.core.appmanager.validator.ValidatorConfig.CheckableConfig; @Component public class ValidatorImpl implements Validator { - private static final Logger LOG = LoggerFactory.getLogger(ValidatorImpl.class); + private final Logger log = LoggerFactory.getLogger(ValidatorImpl.class); - @Reference(// - cardinality = ReferenceCardinality.MULTIPLE, // - policy = ReferencePolicy.DYNAMIC, // - policyOption = ReferencePolicyOption.GREEDY, // - // requires prototype for thread safety - scope = ReferenceScope.PROTOTYPE_REQUIRED // - ) - private volatile List> checkableFactories; + private final CheckableFactory checkableFactory; @Activate - public ValidatorImpl() { + public ValidatorImpl(@Reference CheckableFactory checkableFactory) { + this.checkableFactory = checkableFactory; } @Override - public List getErrorMessages(List checkableConfigs, Language language, - boolean returnImmediate) { + public List getErrorMessages(// + final List checkableConfigs, // + final Language language, // + final boolean returnImmediate // + ) { if (checkableConfigs.isEmpty()) { return emptyList(); } final var errorMessages = new ArrayList(checkableConfigs.size()); for (var config : checkableConfigs) { - // find the componentServiceObjects base on the given configuration name - final var cso = this.checkableFactories.stream()// - .filter(csoCheckable -> { - var sr = csoCheckable.getServiceReference(); - var srName = (String) sr.getProperty(OpenemsConstants.PROPERTY_OSGI_COMPONENT_NAME); - return srName.equals(config.checkableComponentName()); - }).findAny().orElse(null); - - if (cso == null) { - LOG.info("Unable to get Checkable '" + config.checkableComponentName() + "'!"); - continue; - } - - // get the service from the cso - final var checkable = cso.getService(); - - if (checkable == null) { - LOG.info("Unable to get Checkable '" + config.checkableComponentName() + "'!"); - continue; - } + try (final var checkable = this.checkableFactory.useCheckable(config.checkableComponentName())) { + if (checkable == null) { + continue; + } - try { // validate checkable checkable.setProperties(config.properties()); var result = checkable.check(); @@ -78,8 +52,10 @@ public List getErrorMessages(List checkableConfigs, Lan errorMessage = config.invertResult() ? checkable.getInvertedErrorMessage(language) : checkable.getErrorMessage(language); } catch (UnsupportedOperationException e) { - LOG.error("Missing implementation for getting " + (config.invertResult() ? "inverted " : "") - + "error message for check \"" + config.checkableComponentName() + "\"!", e); + this.log.error( + "Missing implementation for getting " + (config.invertResult() ? "inverted " : "") + + "error message for check \"" + config.checkableComponentName() + "\"!", + e); errorMessage = "Check \"" + config.checkableComponentName() + "\" failed."; } errorMessages.add(errorMessage); @@ -87,9 +63,8 @@ public List getErrorMessages(List checkableConfigs, Lan return errorMessages; } } - } finally { - // free checkable from cso - cso.ungetService(checkable); + } catch (Exception e) { + this.log.error("Error while using checkable " + config.checkableComponentName() + "!", e); } } return errorMessages; diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_de.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_de.properties index b43cee111a9..d5edecc681e 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_de.properties +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_de.properties @@ -6,6 +6,9 @@ Validator.Checkable.CheckCardinality.Message.Single = Es ist bereits eine App "{ Validator.Checkable.CheckHome.Message = Diese App benötigt ein Home System. Validator.Checkable.CheckHome.Message.Inverted = Diese App ist nicht für Home Systeme verfügbar. +Validator.Checkable.CheckCommercial92.Message = Diese App benötigt ein Commercial System. +Validator.Checkable.CheckCommercial92.Message.Inverted = Diese App ist nicht für Commercial System verfügbar. + Validator.Checkable.CheckHost.NotReachable = Gerät mit der IP "{0}" ist nicht erreichbar! Validator.Checkable.CheckHost.WrongIp = IP "{0}" ist keine valide IP-Adresse! @@ -13,3 +16,5 @@ Validator.Checkable.CheckNoComponentInstalledOfFactorieId.Message = Komponenten Validator.Checkable.CheckRelayCount.Message = Es sind nicht genug freie Digital-/Relaisausgänge verfügbar.

    Benötigt: {0}
    Verfügbar: {1} Validator.Checkable.CheckRelayCount.Message.AdditionalRelay =

    {1} für weitere Ausgänge erwerben. + +Validator.Checkable.CheckOr.Message = {0} - oder - {1} \ No newline at end of file diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_en.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_en.properties index aa0657e25cd..204566bcbd8 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_en.properties +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_en.properties @@ -6,6 +6,9 @@ Validator.Checkable.CheckCardinality.Message.Single = There is already an app "{ Validator.Checkable.CheckHome.Message = This App requires a Home System. Validator.Checkable.CheckHome.Message.Inverted = This App is not available for Home systems. +Validator.Checkable.CheckCommercial92.Message = This App requires a Commercial System. +Validator.Checkable.CheckCommercial92.Message.Inverted = This App is not available for Commercial System. + Validator.Checkable.CheckHost.NotReachable = Device with IP "{0}" is not reachable! Validator.Checkable.CheckHost.WrongIp = IP "{0}" is not a valid IP-Address! @@ -13,3 +16,5 @@ Validator.Checkable.CheckNoComponentInstalledOfFactorieId.Message = Components w Validator.Checkable.CheckRelayCount.Message = There are not enough Digital-/Relay ports available.

    Required: {0}
    Available: {1} Validator.Checkable.CheckRelayCount.Message.AdditionalRelay =

    Buy {1} for more outputs. + +Validator.Checkable.CheckOr.Message = {0} - or - {1} \ No newline at end of file diff --git a/io.openems.edge.core/src/io/openems/edge/core/componentmanager/ComponentManagerImpl.java b/io.openems.edge.core/src/io/openems/edge/core/componentmanager/ComponentManagerImpl.java index 2dd6105f73c..3029e0d6a04 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/componentmanager/ComponentManagerImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/componentmanager/ComponentManagerImpl.java @@ -49,6 +49,7 @@ import io.openems.common.jsonrpc.request.UpdateComponentConfigRequest; import io.openems.common.jsonrpc.request.UpdateComponentConfigRequest.Property; import io.openems.common.jsonrpc.response.GetEdgeConfigResponse; +import io.openems.common.session.Language; import io.openems.common.session.Role; import io.openems.common.types.ChannelAddress; import io.openems.common.types.EdgeConfig; @@ -368,12 +369,12 @@ public void buildJsonApiRoutes(JsonApiBuilder builder) { Handles a GetStateChannelsOfComponent. """); }, call -> { - // TODO could be used for translating channel texts - // final var user = call.get(EdgeKeys.USER_KEY); + final var user = call.get(EdgeKeys.USER_KEY); + final var lang = user.getLanguage(); final var channels = this.getPossiblyDisabledComponent(call.getRequest().componentId()).channels().stream() // .filter(t -> t.channelDoc().getChannelCategory() == ChannelCategory.STATE) // - .map(ComponentManagerImpl::toChannelRecord) // + .map(channel -> ComponentManagerImpl.toChannelRecord(channel, lang)) // .toList(); return new GetStateChannelsOfComponent.Response(channels); @@ -384,10 +385,12 @@ public void buildJsonApiRoutes(JsonApiBuilder builder) { Handles a GetStateChannelsOfComponent. """); }, call -> { + + final var user = call.get(EdgeKeys.USER_KEY); + final var lang = user.getLanguage(); final var channels = this.getPossiblyDisabledComponent(call.getRequest().componentId()).channels().stream() // - .map(ComponentManagerImpl::toChannelRecord) // + .map(channel -> ComponentManagerImpl.toChannelRecord(channel, lang)) // .toList(); - return new GetChannelsOfComponent.Response(channels); }); @@ -399,7 +402,9 @@ public void buildJsonApiRoutes(JsonApiBuilder builder) { final var request = call.getRequest(); final var channel = this.getChannel(new ChannelAddress(request.componentId(), request.channelId())); - return new GetChannel.Response(toChannelRecord(channel)); + final var user = call.get(EdgeKeys.USER_KEY); + final var lang = user.getLanguage(); + return new GetChannel.Response(toChannelRecord(channel, lang)); }); builder.handleRequest(new GetDigitalInputChannelsOfComponents(), endpoint -> { @@ -408,10 +413,12 @@ public void buildJsonApiRoutes(JsonApiBuilder builder) { """); }, call -> { + final var user = call.get(EdgeKeys.USER_KEY); + final var lang = user.getLanguage(); final var result = this.getEnabledComponentsOfType(DigitalInput.class).stream() // .filter(t -> call.getRequest().componentIds().contains(t.id())) // .collect(toMap(OpenemsComponent::id, t -> Arrays.stream(t.digitalInputChannels()) // - .map(ComponentManagerImpl::toChannelRecord) // + .map(channel -> ComponentManagerImpl.toChannelRecord(channel, lang)) // .toList())); return new GetDigitalInputChannelsOfComponents.Response(result); @@ -450,12 +457,12 @@ public void buildJsonApiRoutes(JsonApiBuilder builder) { }); } - private static ChannelRecord toChannelRecord(Channel channel) { + private static ChannelRecord toChannelRecord(Channel channel, Language language) { return new GetChannelsOfComponent.ChannelRecord(// channel.channelId().id(), // channel.channelDoc().getAccessMode(), // channel.channelDoc().getPersistencePriority(), // - channel.channelDoc().getText(), // + channel.channelDoc().getText(language), // channel.channelDoc().getType(), // channel.channelDoc().getUnit(), // channel.channelDoc().getChannelCategory(), // diff --git a/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHome.java b/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHome.java index f95d671afdf..91ea3e5a532 100644 --- a/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHome.java +++ b/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHome.java @@ -2,9 +2,12 @@ import static io.openems.edge.common.test.DummyUser.DUMMY_ADMIN; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import com.google.gson.JsonObject; @@ -31,7 +34,8 @@ public void beforeEach() throws Exception { Apps::gridOptimizedCharge, // Apps::selfConsumptionOptimization, // Apps::socomecMeter, // - Apps::prepareBatteryExtension // + Apps::prepareBatteryExtension, // + Apps::limiter14a // ); }, null, new PseudoComponentManagerFactory()); @@ -46,52 +50,25 @@ public void testCreateHomeFullSettings() throws Exception { @Test public void testCreateAndUpdateHomeFullSettings() throws Exception { - var fullConfig = JsonUtils.buildJsonObject() // - .addProperty("SAFETY_COUNTRY", "GERMANY") // - .addProperty("RIPPLE_CONTROL_RECEIVER_ACTIV", false) // - .addProperty("MAX_FEED_IN_POWER", 1000) // - .addProperty("FEED_IN_SETTING", "LAGGING_0_95") // - .addProperty("HAS_AC_METER", true) // - .addProperty("HAS_DC_PV1", true) // - .addProperty("DC_PV1_ALIAS", "alias pv 1") // - .addProperty("HAS_DC_PV2", true) // - .addProperty("DC_PV2_ALIAS", "alias pv 2") // - .addProperty("HAS_EMERGENCY_RESERVE", true) // - .addProperty("EMERGENCY_RESERVE_ENABLED", true) // - .addProperty("EMERGENCY_RESERVE_SOC", 15) // - .addProperty("SHADOW_MANAGEMENT_DISABLED", false) // - .build(); var homeInstance = this.createFullHome(); this.appManagerTestBundle.sut.handleUpdateAppInstanceRequest(DUMMY_ADMIN, - new UpdateAppInstance.Request(homeInstance.instanceId, "aliasrename", fullConfig)); + new UpdateAppInstance.Request(homeInstance.instanceId, "aliasrename", fullSettings())); // expect the same as before // make sure every dependency got installed - assertEquals(this.appManagerTestBundle.sut.getInstantiatedApps().size(), 5); + assertEquals(5, this.appManagerTestBundle.sut.getInstantiatedApps().size()); // check properties of created apps for (var instance : this.appManagerTestBundle.sut.getInstantiatedApps()) { - int expectedDependencies; - switch (instance.appId) { - case "App.FENECON.Home": - expectedDependencies = 4; - break; - case "App.PvSelfConsumption.GridOptimizedCharge": - expectedDependencies = 0; - break; - case "App.PvSelfConsumption.SelfConsumptionOptimization": - expectedDependencies = 0; - break; - case "App.Meter.Socomec": - expectedDependencies = 0; - break; - case "App.Ess.PrepareBatteryExtension": - expectedDependencies = 0; - break; - default: - throw new Exception("App with ID[" + instance.appId + "] should not have been created!"); - } + final var expectedDependencies = switch (instance.appId) { + case "App.FENECON.Home" -> 4; + case "App.PvSelfConsumption.GridOptimizedCharge" -> 0; + case "App.PvSelfConsumption.SelfConsumptionOptimization" -> 0; + case "App.Meter.Socomec" -> 0; + case "App.Ess.PrepareBatteryExtension" -> 0; + default -> throw new Exception("App with ID[" + instance.appId + "] should not have been created!"); + }; if (expectedDependencies == 0 && instance.dependencies == null) { continue; } @@ -108,6 +85,7 @@ public void testRemoveAcMeter() throws Exception { .addProperty("RIPPLE_CONTROL_RECEIVER_ACTIV", false) // .addProperty("MAX_FEED_IN_POWER", 1000) // .addProperty("FEED_IN_SETTING", "LAGGING_0_95") // + .addProperty("HAS_ESS_LIMITER_14A", false) // .addProperty("HAS_AC_METER", false) // .addProperty("HAS_DC_PV1", true) // .addProperty("DC_PV1_ALIAS", "alias pv 1") // @@ -123,27 +101,17 @@ public void testRemoveAcMeter() throws Exception { new UpdateAppInstance.Request(homeInstance.instanceId, "aliasrename", configNoMeter)); // expect the same as before // make sure every dependency got installed - assertEquals(this.appManagerTestBundle.sut.getInstantiatedApps().size(), 4); + assertEquals(4, this.appManagerTestBundle.sut.getInstantiatedApps().size()); // check properties of created apps for (var instance : this.appManagerTestBundle.sut.getInstantiatedApps()) { - int expectedDependencies; - switch (instance.appId) { - case "App.FENECON.Home": - expectedDependencies = 3; - break; - case "App.PvSelfConsumption.GridOptimizedCharge": - expectedDependencies = 0; - break; - case "App.PvSelfConsumption.SelfConsumptionOptimization": - expectedDependencies = 0; - break; - case "App.Ess.PrepareBatteryExtension": - expectedDependencies = 0; - break; - default: - throw new Exception("App with ID[" + instance.appId + "] should not have been created!"); - } + final var expectedDependencies = switch (instance.appId) { + case "App.FENECON.Home" -> 3; + case "App.PvSelfConsumption.GridOptimizedCharge" -> 0; + case "App.PvSelfConsumption.SelfConsumptionOptimization" -> 0; + case "App.Ess.PrepareBatteryExtension" -> 0; + default -> throw new Exception("App with ID[" + instance.appId + "] should not have been created!"); + }; if (expectedDependencies == 0 && instance.dependencies == null) { continue; } @@ -194,6 +162,30 @@ public void testFeedInTypeNoLimitation() throws Exception { assertEquals("DISABLE", batteryInverterProps.get("rcrEnable")); } + @Test + @Ignore + public void testEnableLimiter14a() throws Exception { + final var createSettings = fullSettings(); + createSettings.addProperty(FeneconHome.Property.HAS_ESS_LIMITER_14A.name(), false); + + final var createResponse = this.appManagerTestBundle.sut.handleAddAppInstanceRequest(DUMMY_ADMIN, + new AddAppInstance.Request("App.FENECON.Home", "key", "alias", createSettings)); + + assertEquals(4, createResponse.instance().dependencies.size()); + assertFalse(this.appManagerTestBundle.sut.getInstantiatedApps().stream() + .anyMatch(a -> a.appId.equals("App.Ess.Limiter14a"))); + + final var updateSettings = fullSettings(); + createSettings.addProperty(FeneconHome.Property.HAS_ESS_LIMITER_14A.name(), true); + final var updateResponse = this.appManagerTestBundle.sut.handleUpdateAppInstanceRequest(DUMMY_ADMIN, + new UpdateAppInstance.Request(createResponse.instance().instanceId, "alias", updateSettings)); + + assertEquals(5, updateResponse.instance().dependencies.size()); + assertTrue(this.appManagerTestBundle.sut.getInstantiatedApps().stream() + .anyMatch(a -> a.appId.equals("App.Ess.Limiter14a"))); + + } + private final OpenemsAppInstance createFullHome() throws Exception { return createFullHome(this.appManagerTestBundle, DUMMY_ADMIN); } @@ -216,7 +208,7 @@ public static final OpenemsAppInstance createFullHome(AppManagerTestBundle appMa assertEquals(4, response.instance().dependencies.size()); // make sure every dependency got installed - assertEquals(appManagerTestBundle.sut.getInstantiatedApps().size(), 5); + assertEquals(5, appManagerTestBundle.sut.getInstantiatedApps().size()); // check properties of created apps for (var instance : appManagerTestBundle.sut.getInstantiatedApps()) { @@ -252,6 +244,7 @@ public static final JsonObject fullSettings() { .addProperty("RIPPLE_CONTROL_RECEIVER_ACTIV", false) // .addProperty("MAX_FEED_IN_POWER", 1000) // .addProperty("FEED_IN_SETTING", "LAGGING_0_95") // + .addProperty("HAS_ESS_LIMITER_14A", false) // .addProperty("HAS_AC_METER", true) // .addProperty("HAS_DC_PV1", true) // .addProperty("DC_PV1_ALIAS", "alias pv 1") // diff --git a/io.openems.edge.core/test/io/openems/edge/app/timeofusetariff/TestTibber.java b/io.openems.edge.core/test/io/openems/edge/app/timeofusetariff/TestTibber.java index da9a0d9deec..cb4227e2309 100644 --- a/io.openems.edge.core/test/io/openems/edge/app/timeofusetariff/TestTibber.java +++ b/io.openems.edge.core/test/io/openems/edge/app/timeofusetariff/TestTibber.java @@ -11,7 +11,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.concurrent.ExecutionException; import java.util.stream.Stream; import org.junit.Before; @@ -25,11 +24,13 @@ import io.openems.common.jsonrpc.request.CreateComponentConfigRequest; import io.openems.common.jsonrpc.request.UpdateComponentConfigRequest; import io.openems.common.utils.JsonUtils; -import io.openems.edge.app.integratedsystem.TestFeneconHome; import io.openems.edge.core.appmanager.AppManagerTestBundle; import io.openems.edge.core.appmanager.AppManagerTestBundle.PseudoComponentManagerFactory; import io.openems.edge.core.appmanager.Apps; import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance; +import io.openems.edge.core.appmanager.validator.CheckAppsNotInstalled; +import io.openems.edge.core.appmanager.validator.CheckCommercial92; +import io.openems.edge.core.appmanager.validator.CheckHome; public class TestTibber { @@ -40,8 +41,7 @@ public class TestTibber { public void beforeEach() throws Exception { this.appManagerTestBundle = new AppManagerTestBundle(null, null, t -> { return ImmutableList.of(// - this.tibber = Apps.tibber(t), // - Apps.feneconHome(t) // + this.tibber = Apps.tibber(t) // ); }, null, new PseudoComponentManagerFactory()); @@ -52,8 +52,6 @@ public void beforeEach() throws Exception { @Test public void testRemoveAccessToken() throws Exception { - this.installHome(); - final var properties = JsonUtils.buildJsonObject() // .addProperty("ACCESS_TOKEN", "g78aw9ht2n112nb453") // .build(); @@ -83,7 +81,6 @@ public void testRemoveAccessToken() throws Exception { @Test public void testAddChannelToPredictor() throws Exception { this.createPredictor(); - this.installHome(); final var properties = JsonUtils.buildJsonObject() // .addProperty("ACCESS_TOKEN", "g78aw9ht2n112nb453") // @@ -95,7 +92,14 @@ public void testAddChannelToPredictor() throws Exception { } @Test(expected = OpenemsNamedException.class) - public void testOnlyCompatibleWithHome() throws Exception { + public void testOnlyCompatibleWithHomeOrCommercial() throws Exception { + this.appManagerTestBundle.addCheckable(CheckHome.COMPONENT_NAME, + t -> new CheckHome(t, new CheckAppsNotInstalled(this.appManagerTestBundle.sut, + AppManagerTestBundle.getComponentContext(CheckAppsNotInstalled.COMPONENT_NAME)))); + this.appManagerTestBundle.addCheckable(CheckCommercial92.COMPONENT_NAME, + t -> new CheckCommercial92(t, new CheckAppsNotInstalled(this.appManagerTestBundle.sut, + AppManagerTestBundle.getComponentContext(CheckAppsNotInstalled.COMPONENT_NAME)))); + final var properties = JsonUtils.buildJsonObject() // .addProperty("ACCESS_TOKEN", "g78aw9ht2n112nb453") // .build(); @@ -105,7 +109,6 @@ public void testOnlyCompatibleWithHome() throws Exception { @Test public void testSetTokenValue() throws Exception { - this.installHome(); final var properties = JsonUtils.buildJsonObject() // .addProperty("ACCESS_TOKEN", "g78aw9ht2n112nb453") // .build(); @@ -131,7 +134,6 @@ public void testSetTokenValue() throws Exception { @Test public void testUnsetFilterValue() throws Exception { - this.installHome(); final var properties = JsonUtils.buildJsonObject() // .addProperty(Tibber.Property.ACCESS_TOKEN.name(), "g78aw9ht2n112nb453") // .addProperty(Tibber.Property.MULTIPLE_HOMES_CHECK.name(), true) // @@ -170,11 +172,6 @@ private void createPredictor() throws Exception { ))); } - private void installHome() throws InterruptedException, ExecutionException, OpenemsNamedException { - this.appManagerTestBundle.sut.handleAddAppInstanceRequest(DUMMY_ADMIN, - new AddAppInstance.Request("App.FENECON.Home", "key", "alias", TestFeneconHome.minSettings())); - } - private void assertChannelsInPredictor(String... channels) throws OpenemsNamedException { final var existingAddresses = this.getChannelsInPredictor(); final var expectedChannels = Stream.of(channels).collect(toSet()); diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImpSynchronizationTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImpSynchronizationTest.java deleted file mode 100644 index 9b79d51b330..00000000000 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImpSynchronizationTest.java +++ /dev/null @@ -1,199 +0,0 @@ -package io.openems.edge.core.appmanager; - -import static io.openems.edge.common.test.DummyUser.DUMMY_ADMIN; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; - -import com.google.common.collect.Lists; - -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.utils.JsonUtils; -import io.openems.common.utils.ReflectionUtils; -import io.openems.edge.app.evcs.KebaEvcs; -import io.openems.edge.common.test.ComponentTest; -import io.openems.edge.common.test.DummyComponentContext; -import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.core.appmanager.AppManagerTestBundle.CheckablesBundle; -import io.openems.edge.core.appmanager.DummyValidator.TestCheckable; -import io.openems.edge.core.appmanager.dependency.AppManagerAppHelper; -import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance; -import io.openems.edge.core.appmanager.jsonrpc.DeleteAppInstance; -import io.openems.edge.core.appmanager.validator.CheckAppsNotInstalled; -import io.openems.edge.core.appmanager.validator.CheckCardinality; -import io.openems.edge.core.appmanager.validator.CheckHome; -import io.openems.edge.core.appmanager.validator.relaycount.CheckRelayCount; - -public class AppManagerImpSynchronizationTest { - - private AppManagerImpl appManager; - - @Before - public void before() throws Exception { - this.appManager = new AppManagerImpl(); - ReflectionUtils.setAttribute(AppManagerImpl.class, this.appManager, "appValidateWorker", - new AppValidateWorker()); - assertTrue(this.appManager.lockModifyingApps.tryLock()); - this.appManager.lockModifyingApps.unlock(); - assertFalse(this.appManager.waitingForModified); - - final var cm = new DummyConfigurationAdmin(); - final var componentManager = new DummyComponentManager(); - componentManager.setConfigJson(JsonUtils.buildJsonObject() // - .add("components", JsonUtils.buildJsonObject() // - .add("scheduler0", JsonUtils.buildJsonObject() // - .addProperty("factoryId", "Scheduler.AllAlphabetically") // - .add("properties", JsonUtils.buildJsonObject() // - .addProperty("enabled", true) // - .add("controllers.ids", JsonUtils.buildJsonArray() // - .build()) // - .build()) // - .build()) - .build()) - .add("factories", JsonUtils.buildJsonObject() // - .build()) - .build()); - componentManager.setConfigurationAdmin(cm); - - // create config for scheduler - cm.getOrCreateEmptyConfiguration(componentManager.getEdgeConfig().getComponent("scheduler0").get().getPid()); - final var componentUtil = new ComponentUtilImpl(componentManager); - final var appManagerUtil = new AppManagerUtilImpl(componentManager); - final var validator = new DummyValidator(); - - final var checkablesBundle = new CheckablesBundle(// - new TestCheckable(), // - new CheckCardinality(this.appManager, appManagerUtil, - AppManagerTestBundle.getComponentContext(CheckCardinality.COMPONENT_NAME)), // - new CheckRelayCount(componentUtil, - AppManagerTestBundle.getComponentContext(CheckRelayCount.COMPONENT_NAME), null), // - new CheckAppsNotInstalled(this.appManager, - AppManagerTestBundle.getComponentContext(CheckAppsNotInstalled.COMPONENT_NAME)), // - new CheckHome(this.appManager.componentManager, - AppManagerTestBundle.getComponentContext(CheckHome.COMPONENT_NAME), - new CheckAppsNotInstalled(this.appManager, - AppManagerTestBundle.getComponentContext(CheckAppsNotInstalled.COMPONENT_NAME))) // - ); - - validator.setCheckables(checkablesBundle.all()); - - new ComponentTest(this.appManager) // - .addReference("cm", cm) // - .addReference("componentManager", componentManager) // - .addReference("csoAppManagerAppHelper", - AppManagerTestBundle.cso( - new DummyAppManagerAppHelper(componentManager, componentUtil, appManagerUtil))) // - .addReference("validator", validator) // - .addReference("backendUtil", new DummyAppCenterBackendUtil()) // - .addReference("availableApps", Lists.newArrayList(// - new KebaEvcs(componentManager, - AppManagerTestBundle.getComponentContext("App.PvInverter.SolarEdge"), cm, componentUtil) // - )) // - .activate(MyConfig.create() // - .setApps("[]") // - .build()); - - assertTrue(this.appManager.lockModifyingApps.tryLock()); - this.appManager.lockModifyingApps.unlock(); - assertFalse(this.appManager.waitingForModified); - } - - @Test - public void testInstallationOfNotAvailableApp() throws Exception { - try { - this.appManager.handleAddAppInstanceRequest(DUMMY_ADMIN, - new AddAppInstance.Request("someAppId", "key", "alias", JsonUtils.buildJsonObject() // - .build())); - } catch (OpenemsNamedException e) { - // expected - } - - // if app is not existing no need to modify - assertTrue(this.appManager.lockModifyingApps.tryLock()); - this.appManager.lockModifyingApps.unlock(); - assertFalse(this.appManager.waitingForModified); - } - - @Test - public void testRemoveOfNotAvailableInstance() throws Exception { - try { - this.appManager.handleDeleteAppInstanceRequest(DUMMY_ADMIN, - new DeleteAppInstance.Request(UUID.randomUUID())); - } catch (OpenemsNamedException e) { - // expected - } - - // if instance is not existing no need to modify - assertTrue(this.appManager.lockModifyingApps.tryLock()); - this.appManager.lockModifyingApps.unlock(); - assertFalse(this.appManager.waitingForModified); - } - - @Test - public void testSimulateAfterInstallaion() throws Exception { - this.appManager.handleAddAppInstanceRequest(DUMMY_ADMIN, - new AddAppInstance.Request("App.PvInverter.SolarEdge", "key", "alias", JsonUtils.buildJsonObject() // - .build())); - - assertTrue(this.appManager.lockModifyingApps.tryLock()); - assertTrue(this.appManager.waitingForModified); - assertFalse(this.appManager.waitingForModifiedCondition.await(1, TimeUnit.MILLISECONDS)); - this.appManager.lockModifyingApps.unlock(); - } - - @Test - @Ignore - public void testSimulateLockWaitingForModification() throws Exception { - this.appManager.handleAddAppInstanceRequest(DUMMY_ADMIN, - new AddAppInstance.Request("App.PvInverter.SolarEdge", "key", "alias", JsonUtils.buildJsonObject() // - .build())); - - assertTrue(this.appManager.waitingForModified); - - final var second = CompletableFuture.supplyAsync(() -> { - try { - return this.appManager.handleAddAppInstanceRequest(DUMMY_ADMIN, - new AddAppInstance.Request("App.PvInverter.SolarEdge", "key", "alias", - JsonUtils.buildJsonObject() // - .build())); - } catch (OpenemsNamedException e) { - throw new RuntimeException(e); - } - }); - - Thread.sleep(5000); - assertFalse(second.isDone()); - - this.appManager.modified(new DummyComponentContext(), MyConfig.create() // - .setApps("[]") // - .build()); - - Thread.sleep(5000); - assertTrue(second.isDone()); - } - - @Test - public void testSimulateBeforeModified() throws Exception { - // simulate after an instance got created and the configuration was requested to - // update - assertTrue(this.appManager.lockModifyingApps.tryLock()); - this.appManager.waitingForModified = true; - this.appManager.lockModifyingApps.unlock(); - - this.appManager.modified(new DummyComponentContext(), MyConfig.create() // - .setApps("[]") // - .build()); - - assertTrue(this.appManager.lockModifyingApps.tryLock()); - assertFalse(this.appManager.waitingForModified); - } - -} diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImplSynchronizationTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImplSynchronizationTest.java new file mode 100644 index 00000000000..f5f69288d0c --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImplSynchronizationTest.java @@ -0,0 +1,128 @@ +package io.openems.edge.core.appmanager; + +import static io.openems.edge.common.test.DummyUser.DUMMY_ADMIN; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import com.google.common.collect.ImmutableList; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.common.test.DummyComponentContext; +import io.openems.edge.core.appmanager.AppManagerTestBundle.PseudoComponentManagerFactory; +import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance; +import io.openems.edge.core.appmanager.jsonrpc.DeleteAppInstance; + +public class AppManagerImplSynchronizationTest { + + private AppManagerImpl appManager; + + @Before + public void before() throws Exception { + final var appManagerTestBundle = new AppManagerTestBundle(null, null, t -> { + return ImmutableList.of(// + Apps.kebaEvcs(t), // + Apps.solarEdgePvInverter(t) // + ); + }, null, new PseudoComponentManagerFactory(), new AppManagerImpl()); + + this.appManager = appManagerTestBundle.sut; + + assertTrue(this.appManager.lockModifyingApps.tryLock()); + this.appManager.lockModifyingApps.unlock(); + assertFalse(this.appManager.waitingForModified); + } + + @Test + public void testInstallationOfNotAvailableApp() throws Exception { + assertThrows(OpenemsNamedException.class, () -> { + this.appManager.handleAddAppInstanceRequest(DUMMY_ADMIN, + new AddAppInstance.Request("someAppId", "key", "alias", JsonUtils.buildJsonObject() // + .build())); + }); + + // if app is not existing no need to modify + assertTrue(this.appManager.lockModifyingApps.tryLock()); + this.appManager.lockModifyingApps.unlock(); + assertFalse(this.appManager.waitingForModified); + } + + @Test + public void testRemoveOfNotAvailableInstance() throws Exception { + this.appManager.handleDeleteAppInstanceRequest(DUMMY_ADMIN, new DeleteAppInstance.Request(UUID.randomUUID())); + + // if instance is not existing no need to modify + assertTrue(this.appManager.lockModifyingApps.tryLock()); + this.appManager.lockModifyingApps.unlock(); + assertFalse(this.appManager.waitingForModified); + } + + @Test + public void testSimulateAfterInstallation() throws Exception { + assertTrue(this.appManager.lockModifyingApps.tryLock()); + this.appManager.handleAddAppInstanceRequest(DUMMY_ADMIN, + new AddAppInstance.Request("App.PvInverter.SolarEdge", "key", "alias", JsonUtils.buildJsonObject() // + .build())); + + assertTrue(this.appManager.waitingForModified); + assertFalse(this.appManager.waitingForModifiedCondition.await(1, TimeUnit.MILLISECONDS)); + this.appManager.lockModifyingApps.unlock(); + } + + @Test + @Ignore + public void testSimulateLockWaitingForModification() throws Exception { + this.appManager.handleAddAppInstanceRequest(DUMMY_ADMIN, + new AddAppInstance.Request("App.PvInverter.SolarEdge", "key", "alias", JsonUtils.buildJsonObject() // + .build())); + + assertTrue(this.appManager.waitingForModified); + + final var second = CompletableFuture.supplyAsync(() -> { + try { + return this.appManager.handleAddAppInstanceRequest(DUMMY_ADMIN, + new AddAppInstance.Request("App.PvInverter.SolarEdge", "key", "alias", + JsonUtils.buildJsonObject() // + .build())); + } catch (OpenemsNamedException e) { + throw new RuntimeException(e); + } + }); + + Thread.sleep(5000); + assertFalse(second.isDone()); + + this.appManager.modified(new DummyComponentContext(), MyConfig.create() // + .setApps("[]") // + .build()); + + Thread.sleep(5000); + assertTrue(second.isDone()); + } + + @Test + public void testSimulateBeforeModified() throws Exception { + // simulate after an instance got created and the configuration was requested to + // update + assertTrue(this.appManager.lockModifyingApps.tryLock()); + this.appManager.waitingForModified = true; + this.appManager.lockModifyingApps.unlock(); + + this.appManager.modified(new DummyComponentContext(), MyConfig.create() // + .setApps("[]") // + .build()); + + assertTrue(this.appManager.lockModifyingApps.tryLock()); + assertFalse(this.appManager.waitingForModified); + } + +} diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImplTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImplTest.java index 3e74b994890..ca463981dd4 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImplTest.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImplTest.java @@ -323,7 +323,7 @@ public void testFindAppById() { @Test public void testCheckCardinalitySingle() throws Exception { - var checkable = this.appManagerTestBundle.checkablesBundle.checkCardinality(); + var checkable = this.appManagerTestBundle.getCheckCardinality(); checkable.setProperties(new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("openemsApp", this.homeApp) // .build()); @@ -337,7 +337,7 @@ public void testCheckCardinalityMultiple() throws Exception { UUID.randomUUID(), JsonUtils.buildJsonObject().build(), null)); this.appManagerTestBundle.sut.instantiatedApps.add(new OpenemsAppInstance(this.kebaEvcsApp.getAppId(), "alias", UUID.randomUUID(), JsonUtils.buildJsonObject().build(), null)); - var checkable = this.appManagerTestBundle.checkablesBundle.checkCardinality(); + var checkable = this.appManagerTestBundle.getCheckCardinality(); checkable.setProperties(new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("openemsApp", this.kebaEvcsApp) // .build()); @@ -349,12 +349,11 @@ public void testCheckCardinalityMultiple() throws Exception { public void testCheckCardinalitySingleInCategorie() throws Exception { this.appManagerTestBundle.sut.instantiatedApps.add(new OpenemsAppInstance(this.awattarApp.getAppId(), "alias", UUID.randomUUID(), JsonUtils.buildJsonObject().build(), null)); - var checkable = this.appManagerTestBundle.checkablesBundle.checkCardinality(); + var checkable = this.appManagerTestBundle.getCheckCardinality(); checkable.setProperties(new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("openemsApp", this.stromdao) // .build()); assertFalse(checkable.check()); assertNotNull(checkable.getErrorMessage(Language.DEFAULT)); } - } diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java index abcc772227d..1252dc6b574 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java @@ -8,10 +8,12 @@ import java.util.Dictionary; import java.util.Hashtable; import java.util.List; +import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; +import org.osgi.framework.Bundle; import org.osgi.framework.ServiceReference; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentConstants; @@ -21,11 +23,12 @@ import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Modified; -import com.google.common.collect.Lists; +import com.google.common.collect.ImmutableMap; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; +import io.openems.common.OpenemsConstants; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; import io.openems.common.types.EdgeConfig; @@ -57,12 +60,12 @@ import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance.Request; import io.openems.edge.core.appmanager.jsonrpc.DeleteAppInstance; import io.openems.edge.core.appmanager.jsonrpc.UpdateAppInstance; -import io.openems.edge.core.appmanager.validator.CheckAppsNotInstalled; import io.openems.edge.core.appmanager.validator.CheckCardinality; -import io.openems.edge.core.appmanager.validator.CheckHome; +import io.openems.edge.core.appmanager.validator.CheckOr; import io.openems.edge.core.appmanager.validator.Checkable; +import io.openems.edge.core.appmanager.validator.CheckableFactory; import io.openems.edge.core.appmanager.validator.Validator; -import io.openems.edge.core.appmanager.validator.relaycount.CheckRelayCount; +import io.openems.edge.core.appmanager.validator.ValidatorImpl; public class AppManagerTestBundle { @@ -78,12 +81,16 @@ public class AppManagerTestBundle { private final AppValidateWorker appValidateWorker; - public final CheckablesBundle checkablesBundle; - public final TestScheduler scheduler; - public AppManagerTestBundle(JsonObject initialComponentConfig, MyConfig initialAppManagerConfig, - Function> availableAppsSupplier) throws Exception { + private final CheckableFactory checkableFactory = new CheckableFactory(); + private final CheckCardinality checkCardinality; + + public AppManagerTestBundle(// + JsonObject initialComponentConfig, // + MyConfig initialAppManagerConfig, // + Function> availableAppsSupplier // + ) throws Exception { this(initialComponentConfig, initialAppManagerConfig, availableAppsSupplier, null, new DefaultComponentManagerFactory()); } @@ -94,6 +101,18 @@ public AppManagerTestBundle(// Function> availableAppsSupplier, // Consumer additionalComponentConfig, // ComponentManagerFactory componentManagerFactory // + ) throws Exception { + this(initialComponentConfig, initialAppManagerConfig, availableAppsSupplier, additionalComponentConfig, + componentManagerFactory, new AppManagerImplAutoUpdateOnConfigChange()); + } + + public AppManagerTestBundle(// + JsonObject initialComponentConfig, // + MyConfig initialAppManagerConfig, // + Function> availableAppsSupplier, // + Consumer additionalComponentConfig, // + ComponentManagerFactory componentManagerFactory, // + AppManagerImpl impl // ) throws Exception { if (initialComponentConfig == null) { initialComponentConfig = JsonUtils.buildJsonObject() // @@ -158,87 +177,20 @@ public AppManagerTestBundle(// this.componentUtil = new ComponentUtilImpl(this.componentManger); - this.sut = new AppManagerImpl() { - - @Activate - @Override - protected void activate(ComponentContext componentContext, Config config) { - super.activate(componentContext, config); - } - - @Modified - @Override - protected void modified(ComponentContext componentContext, Config config) throws OpenemsNamedException { - super.modified(componentContext, config); - } - - @Deactivate - @Override - protected void deactivate() { - super.deactivate(); - } - - @Override - public AddAppInstance.Response handleAddAppInstanceRequest(User user, Request request, - boolean ignoreBackend) throws OpenemsNamedException { - final var response = super.handleAddAppInstanceRequest(user, request, ignoreBackend); - this.modifyWithCurrentConfig(); - return response; - } - - @Override - public DeleteAppInstance.Response handleDeleteAppInstanceRequest(User user, - DeleteAppInstance.Request request) throws OpenemsNamedException { - final var response = super.handleDeleteAppInstanceRequest(user, request); - this.modifyWithCurrentConfig(); - return response; - } - - @Override - public UpdateAppInstance.Response handleUpdateAppInstanceRequest(User user, - UpdateAppInstance.Request request) throws OpenemsNamedException { - final var response = super.handleUpdateAppInstanceRequest(user, request); - this.modifyWithCurrentConfig(); - return response; - } + this.sut = impl; - private final void modifyWithCurrentConfig() throws OpenemsNamedException { - final var config = MyConfig.create() // - .setApps(this.instantiatedApps.stream() // - .map(OpenemsAppInstance::toJsonObject) // - .collect(JsonUtils.toJsonArray()) // - .toString()) - .setKey("0000-0000-0000-0000") // - .build(); - DummyComponentContext context; - try { - context = DummyComponentContext.from(config); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - throw new OpenemsException(e); - } - this.modified(context, config); - } - - }; componentManagerFactory.afterInit(this.sut, this.cm); this.appManagerUtil = new AppManagerUtilImpl(this.componentManger); this.appCenterBackendUtil = new DummyAppCenterBackendUtil(); ReflectionUtils.setAttribute(this.appManagerUtil.getClass(), this.appManagerUtil, "appManager", this.sut); - this.checkablesBundle = new CheckablesBundle(// - new TestCheckable(), // - new CheckCardinality(this.sut, this.appManagerUtil, - getComponentContext(CheckCardinality.COMPONENT_NAME)), // - new CheckRelayCount(this.componentUtil, getComponentContext(CheckRelayCount.COMPONENT_NAME), null), // - new CheckAppsNotInstalled(this.sut, getComponentContext(CheckAppsNotInstalled.COMPONENT_NAME)), // - new CheckHome(this.componentManger, getComponentContext(CheckHome.COMPONENT_NAME), - new CheckAppsNotInstalled(this.sut, getComponentContext(CheckAppsNotInstalled.COMPONENT_NAME))) // - ); + this.addCheckable(TestCheckable.COMPONENT_NAME, t -> new TestCheckable()); + this.addCheckable(CheckOr.COMPONENT_NAME, t -> new CheckOr(t, this.checkableFactory)); + this.checkCardinality = this.addCheckable(CheckCardinality.COMPONENT_NAME, + t -> new CheckCardinality(this.sut, this.appManagerUtil, t)); - var dummyValidator = new DummyValidator(); - dummyValidator.setCheckables(this.checkablesBundle.all()); - this.validator = dummyValidator; + this.validator = new ValidatorImpl(this.checkableFactory); this.appHelper = new DummyAppManagerAppHelper(this.componentManger, this.componentUtil, this.appManagerUtil); final var csoAppManagerAppHelper = cso((AppManagerAppHelper) this.appHelper); @@ -274,6 +226,27 @@ private final void modifyWithCurrentConfig() throws OpenemsNamedException { this.scheduler = new TestScheduler(this.componentManger); } + /** + * Adds a checkable to the current test bundle. + * + * @param the type of the checkable to add + * @param componentName the component name of the checkable + * @param checkableFactory the factory to get a instance of the checkable + * @return the created checkable + */ + public T addCheckable(// + final String componentName, // + final Function checkableFactory // + ) { + final var checkable = checkableFactory.apply(getComponentContext(componentName)); + this.checkableFactory.bindCso(cso(componentName, checkable)); + return checkable; + } + + public CheckCardinality getCheckCardinality() { + return this.checkCardinality; + } + /** * Calls the modified method. * @@ -449,30 +422,6 @@ public PersistencePredictorAggregateTask addPersistencePredictorAggregateTask() return persistencePredictorAggregateTaskImpl; } - public record CheckablesBundle(// - DummyValidator.TestCheckable checkTest, // - CheckCardinality checkCardinality, // - CheckRelayCount checkRelayCount, // - CheckAppsNotInstalled checkAppsNotInstalled, // - CheckHome checkHome // - ) { - - /** - * Gets all {@link Checkable}. - * - * @return the {@link Checkable} - */ - public final List all() { - return Lists.newArrayList(// - this.checkTest(), // - this.checkCardinality(), // - this.checkRelayCount(), // - this.checkAppsNotInstalled(), // - this.checkHome() // - ); - } - } - /** * Gets the {@link ComponentContext} for an {@link OpenemsApp} of the given * appId. @@ -575,6 +524,65 @@ public void afterInit(AppManagerImpl impl, ConfigurationAdmin cm) { * @return the {@link ComponentServiceObjects} */ public static ComponentServiceObjects cso(T service) { + return cso(null, service); + } + + /** + * Creates a {@link ComponentServiceObjects} of a service. + * + * @param the type of the service + * @param componentName the name of the component + * @param service the service + * @return the {@link ComponentServiceObjects} + */ + public static ComponentServiceObjects cso(String componentName, T service) { + final var sr = new ServiceReference() { + + private final Map properties = ImmutableMap.builder() // + .put(OpenemsConstants.PROPERTY_OSGI_COMPONENT_NAME, + componentName != null ? componentName : service.getClass().getCanonicalName()) // + .build(); + + @Override + public Object getProperty(String key) { + return this.properties.get(key); + } + + @Override + public String[] getPropertyKeys() { + return this.properties.keySet().toArray(String[]::new); + } + + @Override + public Bundle getBundle() { + return null; + } + + @Override + public Bundle[] getUsingBundles() { + return null; + } + + @Override + public boolean isAssignableTo(Bundle bundle, String className) { + return false; + } + + @Override + public int compareTo(Object reference) { + return 0; + } + + @Override + public Dictionary getProperties() { + return null; + } + + @Override + public A adapt(Class type) { + return null; + } + }; return new ComponentServiceObjects() { @Override @@ -589,11 +597,79 @@ public void ungetService(T service) { @Override public ServiceReference getServiceReference() { - // not needed for test - return null; + return sr; } }; } + /** + * This implementation is used to automatically update the call the modified + * method when a changes happens thru a app change. + */ + private static class AppManagerImplAutoUpdateOnConfigChange extends AppManagerImpl { + + /** + * activate, modified, deactivate need to be overwritten because of reflection + * usage in tests. + */ + + @Activate + @Override + protected void activate(ComponentContext componentContext, Config config) { + super.activate(componentContext, config); + } + + @Modified + @Override + protected void modified(ComponentContext componentContext, Config config) throws OpenemsNamedException { + super.modified(componentContext, config); + } + + @Deactivate + @Override + protected void deactivate() { + super.deactivate(); + } + + @Override + public AddAppInstance.Response handleAddAppInstanceRequest(User user, Request request, boolean ignoreBackend) + throws OpenemsNamedException { + final var response = super.handleAddAppInstanceRequest(user, request, ignoreBackend); + this.modifyWithCurrentConfig(); + return response; + } + + @Override + public DeleteAppInstance.Response handleDeleteAppInstanceRequest(User user, DeleteAppInstance.Request request) + throws OpenemsNamedException { + final var response = super.handleDeleteAppInstanceRequest(user, request); + this.modifyWithCurrentConfig(); + return response; + } + + @Override + public UpdateAppInstance.Response handleUpdateAppInstanceRequest(User user, UpdateAppInstance.Request request) + throws OpenemsNamedException { + final var response = super.handleUpdateAppInstanceRequest(user, request); + this.modifyWithCurrentConfig(); + return response; + } + + private final void modifyWithCurrentConfig() throws OpenemsNamedException { + final var config = MyConfig.create() // + .setApps(this.instantiatedApps.stream() // + .map(OpenemsAppInstance::toJsonObject) // + .collect(JsonUtils.toJsonArray()) // + .toString()) + .setKey("0000-0000-0000-0000") // + .build(); + try { + this.modified(DummyComponentContext.from(config), config); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new OpenemsException(e); + } + } + } + } diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java index ca6f501674b..4fa625f043d 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java @@ -19,6 +19,7 @@ import io.openems.edge.app.api.TimedataInfluxDb; import io.openems.edge.app.ess.FixActivePower; import io.openems.edge.app.ess.FixStateOfCharge; +import io.openems.edge.app.ess.Limiter14a; import io.openems.edge.app.ess.PowerPlantController; import io.openems.edge.app.ess.PrepareBatteryExtension; import io.openems.edge.app.evcs.AlpitronicEvcs; @@ -34,13 +35,23 @@ import io.openems.edge.app.integratedsystem.FeneconHome; import io.openems.edge.app.integratedsystem.FeneconHome20; import io.openems.edge.app.integratedsystem.FeneconHome30; +import io.openems.edge.app.integratedsystem.fenecon.commercial.FeneconCommercial92; import io.openems.edge.app.loadcontrol.ManualRelayControl; import io.openems.edge.app.loadcontrol.ThresholdControl; import io.openems.edge.app.meter.CarloGavazziMeter; import io.openems.edge.app.meter.DiscovergyMeter; import io.openems.edge.app.meter.JanitzaMeter; import io.openems.edge.app.meter.MicrocareSdm630Meter; +import io.openems.edge.app.meter.PhoenixContactMeter; +import io.openems.edge.app.meter.PqPlusMeter; import io.openems.edge.app.meter.SocomecMeter; +import io.openems.edge.app.openemshardware.BeagleBoneBlack; +import io.openems.edge.app.openemshardware.Compulab; +import io.openems.edge.app.openemshardware.TechbaseCm3; +import io.openems.edge.app.openemshardware.TechbaseCm4; +import io.openems.edge.app.openemshardware.TechbaseCm4Max; +import io.openems.edge.app.openemshardware.TechbaseCm4s; +import io.openems.edge.app.openemshardware.TechbaseCm4sGen2; import io.openems.edge.app.peakshaving.PeakShaving; import io.openems.edge.app.peakshaving.PhaseAccuratePeakShaving; import io.openems.edge.app.pvinverter.FroniusPvInverter; @@ -59,10 +70,9 @@ import io.openems.edge.app.timeofusetariff.Tibber; import io.openems.edge.common.component.ComponentManager; -public class Apps { +public final class Apps { private Apps() { - super(); } /** @@ -113,6 +123,16 @@ public static final FeneconHome30 feneconHome30(AppManagerTestBundle t) { return app(t, FeneconHome30::new, "App.FENECON.Home.30"); } + /** + * Test method for creating a {@link FeneconCommercial92}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final FeneconCommercial92 feneconCommercial92(AppManagerTestBundle t) { + return app(t, FeneconCommercial92::new, "App.FENECON.Commercial.92"); + } + // TimeOfUseTariff /** @@ -185,6 +205,76 @@ public static final Tibber tibber(AppManagerTestBundle t) { return app(t, Tibber::new, "App.TimeOfUseTariff.Tibber"); } + /** + * Test method for creating a {@link BeagleBoneBlack}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final BeagleBoneBlack beagleBoneBlack(AppManagerTestBundle t) { + return app(t, BeagleBoneBlack::new, "App.OpenemsHardware.BeagleBoneBlack"); + } + + /** + * Test method for creating a {@link Compulab}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final Compulab compulab(AppManagerTestBundle t) { + return app(t, Compulab::new, "App.OpenemsHardware.Compulab"); + } + + /** + * Test method for creating a {@link TechbaseCm3}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final TechbaseCm3 techbaseCm3(AppManagerTestBundle t) { + return app(t, TechbaseCm3::new, "App.OpenemsHardware.CM3"); + } + + /** + * Test method for creating a {@link TechbaseCm4}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final TechbaseCm4 techbaseCm4(AppManagerTestBundle t) { + return app(t, TechbaseCm4::new, "App.OpenemsHardware.CM4"); + } + + /** + * Test method for creating a {@link TechbaseCm4Max}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final TechbaseCm4Max techbaseCm4Max(AppManagerTestBundle t) { + return app(t, TechbaseCm4Max::new, "App.OpenemsHardware.CM4Max"); + } + + /** + * Test method for creating a {@link TechbaseCm4s}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final TechbaseCm4s techbaseCm4s(AppManagerTestBundle t) { + return app(t, TechbaseCm4s::new, "App.OpenemsHardware.CM4S"); + } + + /** + * Test method for creating a {@link TechbaseCm4sGen2}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final TechbaseCm4sGen2 techbaseCm4sGen2(AppManagerTestBundle t) { + return app(t, TechbaseCm4sGen2::new, "App.OpenemsHardware.CM4S.Gen2"); + } + // Test /** @@ -469,6 +559,16 @@ public static final JanitzaMeter janitzaMeter(AppManagerTestBundle t) { return app(t, JanitzaMeter::new, "App.Meter.Janitza"); } + /** + * Test method for creating a {@link PqPlusMeter}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final PqPlusMeter pqPlusMeter(AppManagerTestBundle t) { + return app(t, PqPlusMeter::new, "App.Meter.PqPlus"); + } + /** * Test method for creating a {@link MicrocareSdm630Meter}. * @@ -479,6 +579,16 @@ public static final MicrocareSdm630Meter microcareSdm630Meter(AppManagerTestBund return app(t, MicrocareSdm630Meter::new, "App.Meter.Microcare.Sdm630"); } + /** + * Test method for creating a {@link PhoenixContactMeter}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final PhoenixContactMeter phoenixContactMeter(AppManagerTestBundle t) { + return app(t, PhoenixContactMeter::new, "App.Meter.PhoenixContact"); + } + // PV-Inverter /** @@ -595,6 +705,16 @@ public static final PowerPlantController powerPlantController(AppManagerTestBund return app(t, PowerPlantController::new, "App.Ess.PowerPlantController"); } + /** + * Test method for creating a {@link Limiter14a}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final Limiter14a limiter14a(AppManagerTestBundle t) { + return app(t, Limiter14a::new, "App.Ess.Limiter14a"); + } + private static final T app(AppManagerTestBundle t, DefaultAppConstructor constructor, String appId) { return constructor.create(t.componentManger, AppManagerTestBundle.getComponentContext(appId), t.cm, t.componentUtil); diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyValidator.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyValidator.java index 1297c42b946..03f2548953a 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyValidator.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyValidator.java @@ -1,53 +1,17 @@ package io.openems.edge.core.appmanager; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.function.Supplier; +import org.osgi.service.component.annotations.Component; + import io.openems.common.session.Language; import io.openems.edge.core.appmanager.validator.Checkable; -import io.openems.edge.core.appmanager.validator.Validator; import io.openems.edge.core.appmanager.validator.ValidatorConfig.CheckableConfig; -public class DummyValidator implements Validator { - - private List checkables; - - @Override - public List getErrorMessages(List checkableConfigs, Language language, - boolean returnImmediate) { - var errors = new ArrayList(); - for (var check : checkableConfigs) { - var checkable = this.findCheckableByName(check.checkableComponentName()); - checkable.setProperties(check.properties()); - if (checkable.check() == check.invertResult()) { - errors.add(check.invertResult() ? checkable.getInvertedErrorMessage(language) - : checkable.getErrorMessage(language)); - if (returnImmediate) { - return errors; - } - } - - } - return errors; - } - - private Checkable findCheckableByName(String name) { - return this.checkables.stream() // - .filter(c -> c.getComponentName().equals(name)) // - .findAny().get(); - } - - public void setCheckables(List checkables) { - this.checkables = checkables; - } - - public List getCheckables() { - return this.checkables; - } +public final class DummyValidator { /** * Creates a {@link CheckableConfig} for a test check. @@ -105,6 +69,7 @@ public static CheckableConfig testCheckable(// return testCheckable(check, (String) null, (String) null); } + @Component(name = TestCheckable.COMPONENT_NAME) public static class TestCheckable implements Checkable { public static final String COMPONENT_NAME = "Test.Validator.Checkable.TestCheckable"; @@ -152,4 +117,7 @@ public String getInvertedErrorMessage(Language language) { } + private DummyValidator() { + } + } diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/ResolveOpenemsHardwareTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/ResolveOpenemsHardwareTest.java new file mode 100644 index 00000000000..fc535d9695d --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/ResolveOpenemsHardwareTest.java @@ -0,0 +1,165 @@ +package io.openems.edge.core.appmanager; + +import static io.openems.edge.common.test.DummyUser.DUMMY_ADMIN; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.FileWriter; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import com.google.common.collect.ImmutableList; +import com.google.gson.JsonObject; + +import io.openems.edge.common.test.DummyComponentContext; +import io.openems.edge.common.test.DummyComponentInstance; +import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance; + +public class ResolveOpenemsHardwareTest { + + @Rule + public TemporaryFolder folder = TemporaryFolder.builder() // + .assureDeletion() // + .build(); + + private AppManagerTestBundle appManagerTestBundle; + private OpenemsApp dummyHardwareApp; + private OpenemsApp dummyHardwareApp2; + + @Before + public void setUp() throws Exception { + this.appManagerTestBundle = new AppManagerTestBundle(null, null, t -> { + return ImmutableList.of(// + this.dummyHardwareApp = Apps.techbaseCm4(t), // + this.dummyHardwareApp2 = Apps.techbaseCm3(t) // + ); + }); + System.setProperty("felix.cm.dir", this.folder.getRoot().getPath()); + } + + @Test + public void testSuccessfulInstallation() throws Exception { + this.setHardwareApp(this.dummyHardwareApp.getAppId()); + + final var completed = new CompletableFuture(); + final var context = new DummyComponentContext(new Hashtable(), DummyComponentInstance.create() // + .setDispose(() -> completed.complete(null)) // + .build()); + + final var testExecutor = new TestExecutor(); + final var resolver = new ResolveOpenemsHardware(context, this.appManagerTestBundle.sut, + this.appManagerTestBundle.appManagerUtil, testExecutor); + + resolver.bindApp(this.dummyHardwareApp); + resolver.bindApp(this.dummyHardwareApp2); + testExecutor.runAll(); + + completed.join(); + + assertEquals(1, this.appManagerTestBundle.sut.getInstantiatedApps().size()); + assertFalse(this.appManagerTestBundle.sut.getHardwareMissmatchChannel().getNextValue().get()); + } + + @Test + public void testNotExistingAppId() throws Exception { + this.setHardwareApp("Random.App.Id"); + + final var completed = new CompletableFuture(); + final var context = new DummyComponentContext(new Hashtable(), DummyComponentInstance.create() // + .setDispose(() -> completed.complete(null)) // + .build()); + + final var testExecutor = new TestExecutor(); + final var resolver = new ResolveOpenemsHardware(context, this.appManagerTestBundle.sut, + this.appManagerTestBundle.appManagerUtil, testExecutor); + + resolver.bindApp(this.dummyHardwareApp); + resolver.bindApp(this.dummyHardwareApp2); + testExecutor.runAll(); + + completed.join(); + + assertEquals(0, this.appManagerTestBundle.sut.getInstantiatedApps().size()); + assertTrue(this.appManagerTestBundle.sut.getHardwareMissmatchChannel().getNextValue().get()); + } + + @Test + public void testAppIdNotSet() throws Exception { + final var completed = new CompletableFuture(); + final var context = new DummyComponentContext(new Hashtable(), DummyComponentInstance.create() // + .setDispose(() -> completed.complete(null)) // + .build()); + + final var testExecutor = new TestExecutor(); + final var resolver = new ResolveOpenemsHardware(context, this.appManagerTestBundle.sut, + this.appManagerTestBundle.appManagerUtil, testExecutor); + + resolver.bindApp(this.dummyHardwareApp); + resolver.bindApp(this.dummyHardwareApp2); + testExecutor.runAll(); + + completed.join(); + + assertEquals(0, this.appManagerTestBundle.sut.getInstantiatedApps().size()); + assertFalse(this.appManagerTestBundle.sut.getHardwareMissmatchChannel().getNextValue().get()); + } + + @Test + public void testWrongHardwareAppInstalled() throws Exception { + this.appManagerTestBundle.sut.handleAddAppInstanceRequest(DUMMY_ADMIN, + new AddAppInstance.Request(this.dummyHardwareApp2.getAppId(), "key", null, new JsonObject())); + + this.setHardwareApp(this.dummyHardwareApp.getAppId()); + + final var completed = new CompletableFuture(); + final var context = new DummyComponentContext(new Hashtable(), DummyComponentInstance.create() // + .setDispose(() -> completed.complete(null)) // + .build()); + + final var testExecutor = new TestExecutor(); + final var resolver = new ResolveOpenemsHardware(context, this.appManagerTestBundle.sut, + this.appManagerTestBundle.appManagerUtil, testExecutor); + + resolver.bindApp(this.dummyHardwareApp); + resolver.bindApp(this.dummyHardwareApp2); + testExecutor.runAll(); + + completed.join(); + + assertEquals(1, this.appManagerTestBundle.sut.getInstantiatedApps().size()); + assertTrue(this.appManagerTestBundle.sut.getHardwareMissmatchChannel().getNextValue().get()); + } + + private void setHardwareApp(String appId) throws Exception { + final var hardwareInfoFile = this.folder.newFile(ResolveOpenemsHardware.OPENEMS_HARDWARE_FILE_NAME); + try (final var fw = new FileWriter(hardwareInfoFile)) { + fw.write(ResolveOpenemsHardware.OPENEMS_HARDWARE_APP_KEY + "=" + appId); + } + } + + private static class TestExecutor implements Executor { + + private final List tasks = new ArrayList<>(); + + @Override + public void execute(Runnable command) { + this.tasks.add(command); + } + + public void runAll() { + this.tasks.forEach(Runnable::run); + this.tasks.clear(); + } + + } + +} diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java index 1e5ae260b70..c14160b59af 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java @@ -34,6 +34,7 @@ public void beforeEach() throws Exception { this.apps.add(new TestTranslation(Apps.feneconHome(t), true, TestFeneconHome.fullSettings())); this.apps.add(new TestTranslation(Apps.feneconHome20(t), true, TestFeneconHome20.fullSettings())); this.apps.add(new TestTranslation(Apps.feneconHome30(t), true, TestFeneconHome30.fullSettings())); + this.apps.add(new TestTranslation(Apps.feneconCommercial92(t), true, new JsonObject())); this.apps.add(new TestTranslation(Apps.awattarHourly(t), true, new JsonObject())); this.apps.add(new TestTranslation(Apps.entsoE(t), true, JsonUtils.buildJsonObject() // .addProperty("BIDDING_ZONE", "GERMANY") // @@ -51,6 +52,13 @@ public void beforeEach() throws Exception { this.apps.add(new TestTranslation(Apps.tibber(t), true, JsonUtils.buildJsonObject() // .addProperty("ACCESS_TOKEN", "123456789") // .build())); + this.apps.add(new TestTranslation(Apps.beagleBoneBlack(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.compulab(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.techbaseCm3(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.techbaseCm4(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.techbaseCm4Max(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.techbaseCm4s(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.techbaseCm4sGen2(t), true, new JsonObject())); this.apps.add(new TestTranslation(Apps.modbusTcpApiReadOnly(t), true, new JsonObject())); this.apps.add(new TestTranslation(Apps.modbusTcpApiReadWrite(t), true, JsonUtils.buildJsonObject() // .addProperty("API_TIMEOUT", 60) // @@ -71,14 +79,14 @@ public void beforeEach() throws Exception { this.apps.add(new TestTranslation(Apps.webastoNext(t), true, new JsonObject())); this.apps.add(new TestTranslation(Apps.webastoUnite(t), true, new JsonObject())); this.apps.add(new TestTranslation(Apps.evcsCluster(t), true, new JsonObject())); - this.apps.add(new TestTranslation(Apps.heatPump(t), false, JsonUtils.buildJsonObject() // + this.apps.add(new TestTranslation(Apps.heatPump(t), true, JsonUtils.buildJsonObject() // .addProperty("OUTPUT_CHANNEL_1", "io0/Relay1") // .addProperty("OUTPUT_CHANNEL_2", "io0/Relay2") // .build())); - this.apps.add(new TestTranslation(Apps.combinedHeatAndPower(t), false, JsonUtils.buildJsonObject() // + this.apps.add(new TestTranslation(Apps.combinedHeatAndPower(t), true, JsonUtils.buildJsonObject() // .addProperty("OUTPUT_CHANNEL", "io0/Relay1") // .build())); - this.apps.add(new TestTranslation(Apps.heatingElement(t), false, JsonUtils.buildJsonObject() // + this.apps.add(new TestTranslation(Apps.heatingElement(t), true, JsonUtils.buildJsonObject() // .addProperty("OUTPUT_CHANNEL_PHASE_L1", "io0/Relay1") // .addProperty("OUTPUT_CHANNEL_PHASE_L2", "io0/Relay2") // .addProperty("OUTPUT_CHANNEL_PHASE_L3", "io0/Relay3") // @@ -90,44 +98,46 @@ public void beforeEach() throws Exception { .addProperty("ESS_ID", "ess0") // .addProperty("METER_ID", "meter0") // .build())); - this.apps.add(new TestTranslation(Apps.manualRelayControl(t), false, JsonUtils.buildJsonObject() // + this.apps.add(new TestTranslation(Apps.manualRelayControl(t), true, JsonUtils.buildJsonObject() // .addProperty("OUTPUT_CHANNEL", "io0/Relay1") // .build())); - this.apps.add(new TestTranslation(Apps.thresholdControl(t), false, JsonUtils.buildJsonObject() // + this.apps.add(new TestTranslation(Apps.thresholdControl(t), true, JsonUtils.buildJsonObject() // .add("OUTPUT_CHANNELS", JsonUtils.buildJsonArray().add("io0/Relay1").build()) // .build())); this.apps.add(new TestTranslation(Apps.discovergyMeter(t), false, JsonUtils.buildJsonObject() // .addProperty("EMAIL", "test@test.test") // .addProperty("PASSWORD", "xxxx") // .build())); - this.apps.add(new TestTranslation(Apps.socomecMeter(t), false, JsonUtils.buildJsonObject() // + this.apps.add(new TestTranslation(Apps.socomecMeter(t), true, JsonUtils.buildJsonObject() // .addProperty("MODBUS_ID", "modbus0") // .build())); - this.apps.add(new TestTranslation(Apps.carloGavazziMeter(t), false, JsonUtils.buildJsonObject() // + this.apps.add(new TestTranslation(Apps.carloGavazziMeter(t), true, JsonUtils.buildJsonObject() // .addProperty("MODBUS_ID", "modbus0") // .addProperty("MODBUS_UNIT_ID", 5) // .build())); - this.apps.add(new TestTranslation(Apps.janitzaMeter(t), false, JsonUtils.buildJsonObject() // + this.apps.add(new TestTranslation(Apps.janitzaMeter(t), true, JsonUtils.buildJsonObject() // .addProperty("MODBUS_ID", "modbus0") // .build())); + this.apps.add(new TestTranslation(Apps.pqPlusMeter(t), false, new JsonObject())); + this.apps.add(new TestTranslation(Apps.phoenixContactMeter(t), true, new JsonObject())); this.apps.add(new TestTranslation(Apps.froniusPvInverter(t), false, JsonUtils.buildJsonObject() // .addProperty("MODBUS_ID", "modbus0") // .addProperty("PORT", 502) // .addProperty("MODBUS_UNIT_ID", 1) // .build())); - this.apps.add(new TestTranslation(Apps.kacoPvInverter(t), false, JsonUtils.buildJsonObject() // + this.apps.add(new TestTranslation(Apps.kacoPvInverter(t), true, JsonUtils.buildJsonObject() // .addProperty("MODBUS_ID", "modbus0") // .addProperty("PORT", 502) // .build())); - this.apps.add(new TestTranslation(Apps.kostalPvInverter(t), false, JsonUtils.buildJsonObject() // + this.apps.add(new TestTranslation(Apps.kostalPvInverter(t), true, JsonUtils.buildJsonObject() // .addProperty("MODBUS_ID", "modbus0") // .addProperty("PORT", 502) // .addProperty("MODBUS_UNIT_ID", 1) // .build())); - this.apps.add(new TestTranslation(Apps.smaPvInverter(t), false, JsonUtils.buildJsonObject() // + this.apps.add(new TestTranslation(Apps.smaPvInverter(t), true, JsonUtils.buildJsonObject() // .addProperty("MODBUS_ID", "modbus0") // .build())); - this.apps.add(new TestTranslation(Apps.solarEdgePvInverter(t), false, JsonUtils.buildJsonObject() // + this.apps.add(new TestTranslation(Apps.solarEdgePvInverter(t), true, JsonUtils.buildJsonObject() // .addProperty("MODBUS_ID", "modbus0") // .addProperty("PORT", 502) // .build())); @@ -147,6 +157,10 @@ public void beforeEach() throws Exception { .build())); this.apps.add(new TestTranslation(Apps.powerPlantController(t), true, new JsonObject())); this.apps.add(new TestTranslation(Apps.prepareBatteryExtension(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.limiter14a(t), true, JsonUtils.buildJsonObject() // + .addProperty("ESS_ID", "ess0") // + .addProperty("INPUT_CHANNEL_ADDRESS", "io0/Relay1") // + .build())); return this.apps.stream().map(TestTranslation::app).toList(); }); } diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/validator/CheckHomeTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/validator/CheckHomeTest.java index 6174b2d811d..54504dd2026 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/validator/CheckHomeTest.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/validator/CheckHomeTest.java @@ -35,13 +35,14 @@ public void setUp() throws Exception { Apps.feneconHome30(t) // ); }, null, new PseudoComponentManagerFactory()); - this.checkHome = this.appManagerTestBundle.checkablesBundle.checkHome(); + this.checkHome = this.appManagerTestBundle.addCheckable(CheckHome.COMPONENT_NAME, + t -> new CheckHome(t, new CheckAppsNotInstalled(this.appManagerTestBundle.sut, + AppManagerTestBundle.getComponentContext(CheckAppsNotInstalled.COMPONENT_NAME)))); } @Test public void testCheck() { - final var checkHome = this.appManagerTestBundle.checkablesBundle.checkHome(); - assertFalse(checkHome.check()); + assertFalse(this.checkHome.check()); assertFalse(PropsUtil.isHomeInstalled(this.appManagerTestBundle.appManagerUtil)); } diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Params.java b/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Params.java index b13de0b8546..f49fe773eb9 100644 --- a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Params.java +++ b/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/Params.java @@ -23,7 +23,7 @@ public record Params(// int essTotalEnergy, // /** ESS Energy below a configured Minimum-SoC [Wh] */ int essMinSocEnergy, // - /** ESS Energy below a configured Maximium-SoC [Wh] */ + /** ESS Energy below a configured Maximum-SoC [Wh] */ int essMaxSocEnergy, // /** ESS Initially Available Energy (SoC in [Wh]) */ int essInitialEnergy, // diff --git a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/ParamsUtils.java b/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/ParamsUtils.java index f350cf2ab44..cd1e73e75ec 100644 --- a/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/ParamsUtils.java +++ b/io.openems.edge.energy/src/io/openems/edge/energy/optimizer/ParamsUtils.java @@ -32,7 +32,7 @@ private ParamsUtils() { * predicted consumption energy that cannot be supplied from production. * * @param essMinSocEnergy ESS energy below a configured minimum SoC [Wh] - * @param essMaxSocEnergy ESS energy below a configured maximium SoC [Wh] + * @param essMaxSocEnergy ESS energy below a configured maximum SoC [Wh] * @param productions Production predictions per period * @param consumptions Consumption predictions per period * @param prices Prices per period diff --git a/io.openems.edge.ess.api/src/io/openems/edge/ess/api/ManagedSymmetricEss.java b/io.openems.edge.ess.api/src/io/openems/edge/ess/api/ManagedSymmetricEss.java index 159f8c054ca..616c112b519 100644 --- a/io.openems.edge.ess.api/src/io/openems/edge/ess/api/ManagedSymmetricEss.java +++ b/io.openems.edge.ess.api/src/io/openems/edge/ess/api/ManagedSymmetricEss.java @@ -163,7 +163,7 @@ public void accept(ManagedSymmetricEss ess, Integer value) throws OpenemsNamedEx * */ SET_REACTIVE_POWER_LESS_OR_EQUALS(new IntegerDoc() // - .unit(Unit.VOLT_AMPERE) // + .unit(Unit.VOLT_AMPERE_REACTIVE) // .accessMode(AccessMode.WRITE_ONLY) // .onChannelSetNextWrite(new PowerConstraint("SetReactivePowerLessOrEquals", Phase.ALL, Pwr.REACTIVE, Relationship.LESS_OR_EQUALS))), // @@ -178,7 +178,7 @@ public void accept(ManagedSymmetricEss ess, Integer value) throws OpenemsNamedEx * */ SET_REACTIVE_POWER_GREATER_OR_EQUALS(new IntegerDoc() // - .unit(Unit.WATT) // + .unit(Unit.VOLT_AMPERE_REACTIVE) // .accessMode(AccessMode.WRITE_ONLY) // .onChannelSetNextWrite(new PowerConstraint("SetReactivePowerGreaterOrEquals", Phase.ALL, Pwr.REACTIVE, Relationship.GREATER_OR_EQUALS))), // diff --git a/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/solver/nearequal/SolveNearEqual.java b/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/solver/nearequal/SolveNearEqual.java index 093dc19fab8..19e01b3e06a 100644 --- a/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/solver/nearequal/SolveNearEqual.java +++ b/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/solver/nearequal/SolveNearEqual.java @@ -30,7 +30,7 @@ public class SolveNearEqual { */ public PointValuePair solve(int totalVariables) { - // If the SetPower is greater than Sum of inidividual upper bound, then return + // If the SetPower is greater than Sum of individual upper bound, then return // the upper bound if (this.powerSetValue > Arrays.stream(this.upperBound).sum()) { return new PointValuePair(this.upperBound, 0.0); diff --git a/io.openems.edge.evcs.alpitronic.hypercharger/src/io/openems/edge/evcs/hypercharger/AvailableState.java b/io.openems.edge.evcs.alpitronic.hypercharger/src/io/openems/edge/evcs/hypercharger/AvailableState.java index 47a5baf3168..9ba99066d73 100644 --- a/io.openems.edge.evcs.alpitronic.hypercharger/src/io/openems/edge/evcs/hypercharger/AvailableState.java +++ b/io.openems.edge.evcs.alpitronic.hypercharger/src/io/openems/edge/evcs/hypercharger/AvailableState.java @@ -14,7 +14,7 @@ public enum AvailableState implements OptionsEnum { SUSPENDED_EV(4, "Suspended electric vehicle"), // SUSPENDED_EV_SE(5, "Suspended electric vehicle se"), // FINISHING(6, "Finishing"), // - RESERVED(7, "Reseved"), // + RESERVED(7, "Reserved"), // UNAVAILABLE(8, "Unavailable"), // UNAVAILABLE_FW_UPDATE(9, "Unavailable firmware update"), // FAULTED(10, "Faulted"), // diff --git a/io.openems.edge.evcs.cluster/readme.adoc b/io.openems.edge.evcs.cluster/readme.adoc index 81558b7e2e4..fcd9b961ce4 100644 --- a/io.openems.edge.evcs.cluster/readme.adoc +++ b/io.openems.edge.evcs.cluster/readme.adoc @@ -1,6 +1,6 @@ = EVCS Cluster -Distributes the charging power (Depending on the implementation) to the priorized charging stations. +Distributes the charging power (Depending on the implementation) to the prioritized charging stations. The implementations calculate the maximum power that can be used by all charging stations. Possible Cluster implementations: diff --git a/io.openems.edge.evcs.webasto.next/src/io/openems/edge/evcs/webasto/next/enums/EvseErrorCode.java b/io.openems.edge.evcs.webasto.next/src/io/openems/edge/evcs/webasto/next/enums/EvseErrorCode.java index b8656c5abd1..7bf947b82f8 100644 --- a/io.openems.edge.evcs.webasto.next/src/io/openems/edge/evcs/webasto/next/enums/EvseErrorCode.java +++ b/io.openems.edge.evcs.webasto.next/src/io/openems/edge/evcs/webasto/next/enums/EvseErrorCode.java @@ -10,7 +10,7 @@ public enum EvseErrorCode implements OptionsEnum { EV_COMMUNICATION_ERROR(3, "EV communication error"), // OVER_VOLTAGE(4, "Over Voltage"), // UNDER_VOLTAGE(5, "Under Voltage"), // - OVER_CURRENT_FALIURE(6, "Over current faliure"), // + OVER_CURRENT_FAILURE(6, "Over current failure"), // OTHER_ERROR(7, "Other error"), // GROUND_FAILURE(8, "Ground failure"), // RCD_MODULE_ERROR(9, "Error RCD modulel"), // diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java index 134e6f5561c..3fcfec14317 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java @@ -287,7 +287,22 @@ protected final ModbusProtocol defineModbusProtocol() { ), new FC3ReadRegistersTask(35250, Priority.LOW, // - m(new BitsWordElement(35250, this) // + + /* + * Table 8-30 Grid Detailed WARNING (35250 - 35253, U64) + */ + new DummyRegisterElement(35250, 35251), // + m(new BitsWordElement(35252, this) // + .bit(0, GoodWe.ChannelId.STATE_86) // + .bit(1, GoodWe.ChannelId.STATE_87) // + .bit(2, GoodWe.ChannelId.STATE_88) // + .bit(3, GoodWe.ChannelId.STATE_89) // + .bit(4, GoodWe.ChannelId.STATE_90) // + .bit(5, GoodWe.ChannelId.STATE_91) // + .bit(6, GoodWe.ChannelId.STATE_92) // + .bit(7, GoodWe.ChannelId.STATE_93) // + ), // + m(new BitsWordElement(35253, this) // .bit(0, GoodWe.ChannelId.STATE_70) // .bit(1, GoodWe.ChannelId.STATE_71) // .bit(2, GoodWe.ChannelId.STATE_72) // @@ -304,18 +319,21 @@ protected final ModbusProtocol defineModbusProtocol() { .bit(13, GoodWe.ChannelId.STATE_83) // .bit(14, GoodWe.ChannelId.STATE_84) // .bit(15, GoodWe.ChannelId.STATE_85)), // - m(new BitsWordElement(35251, this) // - .bit(0, GoodWe.ChannelId.STATE_86) // - .bit(1, GoodWe.ChannelId.STATE_87) // - .bit(2, GoodWe.ChannelId.STATE_88) // - .bit(3, GoodWe.ChannelId.STATE_89) // - .bit(4, GoodWe.ChannelId.STATE_90) // - .bit(5, GoodWe.ChannelId.STATE_91) // - .bit(6, GoodWe.ChannelId.STATE_92) // - .bit(7, GoodWe.ChannelId.STATE_93) // + + /* + * Table 8-31 Inverter detailed error (35254 - 35257, U64) + */ + new DummyRegisterElement(35254, 35255), // + m(new BitsWordElement(35256, this) // + .bit(0, GoodWe.ChannelId.STATE_110) // + .bit(1, GoodWe.ChannelId.STATE_111) // + .bit(2, GoodWe.ChannelId.STATE_112) // + .bit(3, GoodWe.ChannelId.STATE_113) // + .bit(4, GoodWe.ChannelId.STATE_114) // + .bit(5, GoodWe.ChannelId.STATE_115) // + .bit(6, GoodWe.ChannelId.STATE_116) // ), // - new DummyRegisterElement(35252, 35253), // - m(new BitsWordElement(35254, this) // + m(new BitsWordElement(35257, this) // .bit(0, GoodWe.ChannelId.STATE_94) // .bit(1, GoodWe.ChannelId.STATE_95) // .bit(2, GoodWe.ChannelId.STATE_96) // @@ -332,17 +350,19 @@ protected final ModbusProtocol defineModbusProtocol() { .bit(13, GoodWe.ChannelId.STATE_107) // .bit(14, GoodWe.ChannelId.STATE_108) // .bit(15, GoodWe.ChannelId.STATE_109)), // - m(new BitsWordElement(35255, this) // - .bit(0, GoodWe.ChannelId.STATE_110) // - .bit(1, GoodWe.ChannelId.STATE_111) // - .bit(2, GoodWe.ChannelId.STATE_112) // - .bit(3, GoodWe.ChannelId.STATE_113) // - .bit(4, GoodWe.ChannelId.STATE_114) // - .bit(5, GoodWe.ChannelId.STATE_115) // - .bit(6, GoodWe.ChannelId.STATE_116) // + + /* + * Table 8-32 Inverter detailed status (35258 - 35261, U64) + */ + new DummyRegisterElement(35258, 35259), // + m(new BitsWordElement(35260, this) // + .bit(0, GoodWe.ChannelId.STATE_133) // + .bit(1, GoodWe.ChannelId.STATE_134) // + .bit(2, GoodWe.ChannelId.STATE_135) // + .bit(3, GoodWe.ChannelId.STATE_136) // + .bit(4, GoodWe.ChannelId.STATE_137) // ), // - new DummyRegisterElement(35256, 35257), // - m(new BitsWordElement(35258, this) // + m(new BitsWordElement(35261, this) // .bit(0, GoodWe.ChannelId.STATE_117) // .bit(1, GoodWe.ChannelId.STATE_118) // .bit(2, GoodWe.ChannelId.STATE_119) // @@ -359,14 +379,7 @@ protected final ModbusProtocol defineModbusProtocol() { .bit(13, GoodWe.ChannelId.STATE_130) // .bit(14, GoodWe.ChannelId.STATE_131) // .bit(15, GoodWe.ChannelId.STATE_132)), // - m(new BitsWordElement(35259, this) // - .bit(0, GoodWe.ChannelId.STATE_133) // - .bit(1, GoodWe.ChannelId.STATE_134) // - .bit(2, GoodWe.ChannelId.STATE_135) // - .bit(3, GoodWe.ChannelId.STATE_136) // - .bit(4, GoodWe.ChannelId.STATE_137) // - ), // - new DummyRegisterElement(35260, 35267), // + new DummyRegisterElement(35262, 35267), // m(GoodWe.ChannelId.MAX_GRID_FREQ_WITHIN_1_MINUTE, new UnsignedWordElement(35268), SCALE_FACTOR_MINUS_2), // m(GoodWe.ChannelId.MIN_GRID_FREQ_WITHIN_1_MINUTE, new UnsignedWordElement(35269), diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java index c8e8d372118..aa090eaa2b3 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java @@ -525,92 +525,92 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId STATE_57(Doc.of(Level.INFO).text("Charging under voltage 3")), // // Table 8-8 BMS Warning Code - STATE_58(Doc.of(Level.WARNING).text("Charging over voltage 1 ")), // - STATE_59(Doc.of(Level.WARNING).text("Discharging under voltage 1 ")), // - STATE_60(Doc.of(Level.WARNING).text("Cell high temperature 1 ")), // - STATE_61(Doc.of(Level.WARNING).text("Cell low temperature 1 ")), // - STATE_62(Doc.of(Level.WARNING).text("Charging over current 1 ")), // - STATE_63(Doc.of(Level.WARNING).text("Discharging over current 1 ")), // - STATE_64(Doc.of(Level.WARNING).text("Communication failure 1 ")), // - STATE_65(Doc.of(Level.WARNING).text("System reboot ")), // - STATE_66(Doc.of(Level.WARNING).text("Cell imbalance")), // - STATE_67(Doc.of(Level.WARNING).text("System low temperature 1 ")), // - STATE_68(Doc.of(Level.WARNING).text("System low temperature 1 ")), // - STATE_69(Doc.of(Level.WARNING).text("System high temperature")), // - - // Table 8-30 Grid Detailed Fault - STATE_70(Doc.of(Level.FAULT).text("Power outage")), // - STATE_71(Doc.of(Level.FAULT).text("Grid undervoltage first level fault")), // - STATE_72(Doc.of(Level.FAULT).text("Grid undervoltage second level fault")), // - STATE_73(Doc.of(Level.FAULT).text("Grid undervoltage third level fault")), // - STATE_74(Doc.of(Level.FAULT).text("Grid overvoltage first level fault")), // - STATE_75(Doc.of(Level.FAULT).text("Grid overvoltage second level fault")), // - STATE_76(Doc.of(Level.FAULT).text("Grid overvoltage third level fault")), // - STATE_77(Doc.of(Level.FAULT).text("Grid average voltage high fault")), // - STATE_78(Doc.of(Level.FAULT).text("Grid underfrequency first level fault")), // - STATE_79(Doc.of(Level.FAULT).text("Grid underfrequency second level fault")), // - STATE_80(Doc.of(Level.FAULT).text("Islanding protection underfrequency fault")), // - STATE_81(Doc.of(Level.FAULT).text("Grid overfrequency first level fault")), // - STATE_82(Doc.of(Level.FAULT).text("Grid overfrequency second level fault")), // - STATE_83(Doc.of(Level.FAULT).text("Islanding protection overfrequency fault")), // - STATE_84(Doc.of(Level.FAULT).text("Grid frequency shift fault")), // - STATE_85(Doc.of(Level.FAULT).text("Grid waveform check fault")), // - STATE_86(Doc.of(Level.FAULT).text("Grid line voltage fault flag")), // - STATE_87(Doc.of(Level.FAULT).text("Grid low voltage ride-through flag")), // - STATE_88(Doc.of(Level.FAULT).text("Grid high voltage ride-through flag")), // - STATE_89(Doc.of(Level.FAULT).text("Grid voltage exceeds the upper sampling limit")), // - STATE_90(Doc.of(Level.FAULT).text("Grid connection voltage high")), // - STATE_91(Doc.of(Level.FAULT).text("Grid connection voltage low")), // - STATE_92(Doc.of(Level.FAULT).text("Grid connection frequency high")), // - STATE_93(Doc.of(Level.FAULT).text("Grid connection frequency low")), // + STATE_58(Doc.of(OpenemsType.BOOLEAN).text("Charging over voltage 1 ")), // + STATE_59(Doc.of(OpenemsType.BOOLEAN).text("Discharging under voltage 1 ")), // + STATE_60(Doc.of(OpenemsType.BOOLEAN).text("Cell high temperature 1 ")), // + STATE_61(Doc.of(OpenemsType.BOOLEAN).text("Cell low temperature 1 ")), // + STATE_62(Doc.of(OpenemsType.BOOLEAN).text("Charging over current 1 ")), // + STATE_63(Doc.of(OpenemsType.BOOLEAN).text("Discharging over current 1 ")), // + STATE_64(Doc.of(OpenemsType.BOOLEAN).text("Communication failure 1 ")), // + STATE_65(Doc.of(OpenemsType.BOOLEAN).text("System reboot ")), // + STATE_66(Doc.of(OpenemsType.BOOLEAN).text("Cell imbalance")), // + STATE_67(Doc.of(OpenemsType.BOOLEAN).text("System low temperature 1 ")), // + STATE_68(Doc.of(OpenemsType.BOOLEAN).text("System low temperature 1 ")), // + STATE_69(Doc.of(OpenemsType.BOOLEAN).text("System high temperature")), // + + // Table 8-30 Grid Detailed WARNING + STATE_70(Doc.of(OpenemsType.BOOLEAN).text("Power outage")), // + STATE_71(Doc.of(OpenemsType.BOOLEAN).text("Grid undervoltage first level WARNING")), // + STATE_72(Doc.of(OpenemsType.BOOLEAN).text("Grid undervoltage second level WARNING")), // + STATE_73(Doc.of(OpenemsType.BOOLEAN).text("Grid undervoltage third level WARNING")), // + STATE_74(Doc.of(OpenemsType.BOOLEAN).text("Grid overvoltage first level WARNING")), // + STATE_75(Doc.of(OpenemsType.BOOLEAN).text("Grid overvoltage second level WARNING")), // + STATE_76(Doc.of(OpenemsType.BOOLEAN).text("Grid overvoltage third level WARNING")), // + STATE_77(Doc.of(OpenemsType.BOOLEAN).text("Grid average voltage high WARNING")), // + STATE_78(Doc.of(OpenemsType.BOOLEAN).text("Grid underfrequency first level WARNING")), // + STATE_79(Doc.of(OpenemsType.BOOLEAN).text("Grid underfrequency second level WARNING")), // + STATE_80(Doc.of(OpenemsType.BOOLEAN).text("Islanding protection underfrequency WARNING")), // + STATE_81(Doc.of(OpenemsType.BOOLEAN).text("Grid overfrequency first level WARNING")), // + STATE_82(Doc.of(OpenemsType.BOOLEAN).text("Grid overfrequency second level WARNING")), // + STATE_83(Doc.of(OpenemsType.BOOLEAN).text("Islanding protection overfrequency WARNING")), // + STATE_84(Doc.of(OpenemsType.BOOLEAN).text("Grid frequency shift WARNING")), // + STATE_85(Doc.of(OpenemsType.BOOLEAN).text("Grid waveform check WARNING")), // + STATE_86(Doc.of(OpenemsType.BOOLEAN).text("Grid line voltage WARNING flag")), // + STATE_87(Doc.of(OpenemsType.BOOLEAN).text("Grid low voltage ride-through flag")), // + STATE_88(Doc.of(OpenemsType.BOOLEAN).text("Grid high voltage ride-through flag")), // + STATE_89(Doc.of(OpenemsType.BOOLEAN).text("Grid voltage exceeds the upper sampling limit")), // + STATE_90(Doc.of(OpenemsType.BOOLEAN).text("Grid connection voltage high")), // + STATE_91(Doc.of(OpenemsType.BOOLEAN).text("Grid connection voltage low")), // + STATE_92(Doc.of(OpenemsType.BOOLEAN).text("Grid connection frequency high")), // + STATE_93(Doc.of(OpenemsType.BOOLEAN).text("Grid connection frequency low")), // // Table 8-31 Inverter detailed error - STATE_94(Doc.of(Level.FAULT).text("LLC hardware over current")), // - STATE_95(Doc.of(Level.FAULT).text("Battery boost hardware over current")), // - STATE_96(Doc.of(Level.FAULT).text("Battery boost software over current")), // - STATE_97(Doc.of(Level.FAULT).text("Battery bms fault")), // - STATE_98(Doc.of(Level.FAULT).text("Battery bms discharge disable")), // - STATE_99(Doc.of(Level.FAULT).text("Battery current rms over current")), // - STATE_100(Doc.of(Level.FAULT).text("Off-grid mode exceeds bms current limit")), // - STATE_101(Doc.of(Level.FAULT).text("Bus voltage soft start failed")), // - STATE_102(Doc.of(Level.FAULT).text("Bus voltage is too low")), // - STATE_103(Doc.of(Level.FAULT).text("Bus voltage is too high")), // - STATE_104(Doc.of(Level.FAULT).text("Inverter hardware over current")), // - STATE_105(Doc.of(Level.FAULT).text("Inverter software over current")), // - STATE_106(Doc.of(Level.FAULT).text("Pv boost hardware over current")), // - STATE_107(Doc.of(Level.FAULT).text("Pv boost software over current")), // - STATE_108(Doc.of(Level.FAULT).text("Grid back flow")), // - STATE_109(Doc.of(Level.FAULT).text("Off-grid mode battery voltage is too low")), // - STATE_110(Doc.of(Level.FAULT).text("Off-grid mode AC voltage is too low")), // - STATE_111(Doc.of(Level.FAULT).text("Off-grid mode AC voltage is too high")), // - STATE_112(Doc.of(Level.FAULT).text("Backup over load")), // - STATE_113(Doc.of(Level.FAULT).text("Off-grid zero error")), // - STATE_114(Doc.of(Level.FAULT).text("Power fast retrack error")), // - STATE_115(Doc.of(Level.FAULT).text("Bypass relay switch error")), // - STATE_116(Doc.of(Level.FAULT).text("Backup load relay switch error")), // + STATE_94(Doc.of(OpenemsType.BOOLEAN).text("LLC hardware over current")), // + STATE_95(Doc.of(OpenemsType.BOOLEAN).text("Battery boost hardware over current")), // + STATE_96(Doc.of(OpenemsType.BOOLEAN).text("Battery boost software over current")), // + STATE_97(Doc.of(OpenemsType.BOOLEAN).text("Battery bms WARNING")), // + STATE_98(Doc.of(OpenemsType.BOOLEAN).text("Battery bms discharge disable")), // + STATE_99(Doc.of(OpenemsType.BOOLEAN).text("Battery current rms over current")), // + STATE_100(Doc.of(OpenemsType.BOOLEAN).text("Off-grid mode exceeds bms current limit")), // + STATE_101(Doc.of(OpenemsType.BOOLEAN).text("Bus voltage soft start failed")), // + STATE_102(Doc.of(OpenemsType.BOOLEAN).text("Bus voltage is too low")), // + STATE_103(Doc.of(OpenemsType.BOOLEAN).text("Bus voltage is too high")), // + STATE_104(Doc.of(OpenemsType.BOOLEAN).text("Inverter hardware over current")), // + STATE_105(Doc.of(OpenemsType.BOOLEAN).text("Inverter software over current")), // + STATE_106(Doc.of(OpenemsType.BOOLEAN).text("Pv boost hardware over current")), // + STATE_107(Doc.of(OpenemsType.BOOLEAN).text("Pv boost software over current")), // + STATE_108(Doc.of(OpenemsType.BOOLEAN).text("Grid back flow")), // + STATE_109(Doc.of(OpenemsType.BOOLEAN).text("Off-grid mode battery voltage is too low")), // + STATE_110(Doc.of(OpenemsType.BOOLEAN).text("Off-grid mode AC voltage is too low")), // + STATE_111(Doc.of(OpenemsType.BOOLEAN).text("Off-grid mode AC voltage is too high")), // + STATE_112(Doc.of(OpenemsType.BOOLEAN).text("Backup over load")), // + STATE_113(Doc.of(OpenemsType.BOOLEAN).text("Off-grid zero error")), // + STATE_114(Doc.of(OpenemsType.BOOLEAN).text("Power fast retrack error")), // + STATE_115(Doc.of(OpenemsType.BOOLEAN).text("Bypass relay switch error")), // + STATE_116(Doc.of(OpenemsType.BOOLEAN).text("Backup load relay switch error")), // // Table 8-32 Inverter detailed status - STATE_117(Doc.of(Level.INFO).text("Over frequency curve running")), // - STATE_118(Doc.of(Level.INFO).text("Under frequency curve running")), // - STATE_119(Doc.of(Level.INFO).text("Frequency curve exiting recovery")), // - STATE_120(Doc.of(Level.INFO).text("PU over voltage curve running")), // - STATE_121(Doc.of(Level.INFO).text("PU under voltage curve running")), // - STATE_122(Doc.of(Level.INFO).text("QU curve running")), // - STATE_123(Doc.of(Level.INFO).text("PF curve running")), // - STATE_124(Doc.of(Level.INFO).text("Fixed PF is set")), // - STATE_125(Doc.of(Level.INFO).text("Fixed reactive power is set")), // - STATE_126(Doc.of(Level.INFO).text("Inverter over temp,derating curve operation")), // - STATE_127(Doc.of(Level.INFO).text("Australian DRED electricity sale status")), // - STATE_128(Doc.of(Level.INFO).text("Australian DRED purchase status")), // - STATE_129(Doc.of(Level.INFO).text("Active power limit set")), // - STATE_130(Doc.of(Level.INFO).text("70 percent derating (Germany) has been opened")), // - STATE_131(Doc.of(Level.INFO).text("CEI021 selftest running")), // - STATE_132(Doc.of(Level.INFO).text("Inverter first level over voltage derate")), // - STATE_133(Doc.of(Level.INFO).text("Force off grid flag")), // - STATE_134(Doc.of(Level.INFO).text("Force stop mode flag")), // - STATE_135(Doc.of(Level.INFO).text("Pv charge, off backup output flag")), // - STATE_136(Doc.of(Level.INFO).text("QU curve over voltage flag")), // - STATE_137(Doc.of(Level.INFO).text("QU curve under voltage flag")), // + STATE_117(Doc.of(OpenemsType.BOOLEAN).text("Over frequency curve running")), // + STATE_118(Doc.of(OpenemsType.BOOLEAN).text("Under frequency curve running")), // + STATE_119(Doc.of(OpenemsType.BOOLEAN).text("Frequency curve exiting recovery")), // + STATE_120(Doc.of(OpenemsType.BOOLEAN).text("PU over voltage curve running")), // + STATE_121(Doc.of(OpenemsType.BOOLEAN).text("PU under voltage curve running")), // + STATE_122(Doc.of(OpenemsType.BOOLEAN).text("QU curve running")), // + STATE_123(Doc.of(OpenemsType.BOOLEAN).text("PF curve running")), // + STATE_124(Doc.of(OpenemsType.BOOLEAN).text("Fixed PF is set")), // + STATE_125(Doc.of(OpenemsType.BOOLEAN).text("Fixed reactive power is set")), // + STATE_126(Doc.of(OpenemsType.BOOLEAN).text("Inverter over temp,derating curve operation")), // + STATE_127(Doc.of(OpenemsType.BOOLEAN).text("Australian DRED electricity sale status")), // + STATE_128(Doc.of(OpenemsType.BOOLEAN).text("Australian DRED purchase status")), // + STATE_129(Doc.of(OpenemsType.BOOLEAN).text("Active power limit set")), // + STATE_130(Doc.of(OpenemsType.BOOLEAN).text("70 percent derating (Germany) has been opened")), // + STATE_131(Doc.of(OpenemsType.BOOLEAN).text("CEI021 selftest running")), // + STATE_132(Doc.of(OpenemsType.BOOLEAN).text("Inverter first level over voltage derate")), // + STATE_133(Doc.of(OpenemsType.BOOLEAN).text("Force off grid flag")), // + STATE_134(Doc.of(OpenemsType.BOOLEAN).text("Force stop mode flag")), // + STATE_135(Doc.of(OpenemsType.BOOLEAN).text("Pv charge, off backup output flag")), // + STATE_136(Doc.of(OpenemsType.BOOLEAN).text("QU curve over voltage flag")), // + STATE_137(Doc.of(OpenemsType.BOOLEAN).text("QU curve under voltage flag")), // // BMS Information BATTERY_PROTOCOL(Doc.of(BatteryProtocol.values())), // diff --git a/io.openems.edge.io.gpio/src/io/openems/edge/io/gpio/linuxfs/Gpio.java b/io.openems.edge.io.gpio/src/io/openems/edge/io/gpio/linuxfs/Gpio.java index 7391d7c4a91..4f9f500d030 100644 --- a/io.openems.edge.io.gpio/src/io/openems/edge/io/gpio/linuxfs/Gpio.java +++ b/io.openems.edge.io.gpio/src/io/openems/edge/io/gpio/linuxfs/Gpio.java @@ -93,7 +93,7 @@ private synchronized void writeFile(String filename, String value) throws Openem } else if (msg.contains("busy")) { throw new OpenemsException("Skipping write to GPIO pin [" + this.pinNumber + "]: device is busy."); } else { - throw new OpenemsException("Unkown error writing GPIO file: " + msg); + throw new OpenemsException("Unknown error writing GPIO file: " + msg); } } } diff --git a/io.openems.edge.katek.edcom/src/com/ed/data/BatteryData.java b/io.openems.edge.katek.edcom/src/com/ed/data/BatteryData.java index 7760611ec65..c08f72d189b 100644 --- a/io.openems.edge.katek.edcom/src/com/ed/data/BatteryData.java +++ b/io.openems.edge.katek.edcom/src/com/ed/data/BatteryData.java @@ -69,7 +69,7 @@ public final class BatteryData implements DataSet { public final DspVar bms_u_cell_max_total; /** - * Battery mininum cell temperature [°C] + * Battery minimum cell temperature [°C] */ public final DspVar bms_Tmin_total; diff --git a/io.openems.edge.katek.edcom/src/com/ed/data/History.java b/io.openems.edge.katek.edcom/src/com/ed/data/History.java index 47beb589bef..596a56ecf33 100644 --- a/io.openems.edge.katek.edcom/src/com/ed/data/History.java +++ b/io.openems.edge.katek.edcom/src/com/ed/data/History.java @@ -408,7 +408,7 @@ public synchronized SortedMap> getHistoryDay() th *

    * All recorded performance values in per hour of the requested day * sorted by hour in ascending order. The values are stored in arrays of - * lenght 30 and 12. Arrays of length 30 represent 2 minute average + * length 30 and 12. Arrays of length 30 represent 2 minute average * values, arrays of 12 represent 5 minute average values. *

    *

    diff --git a/io.openems.edge.katek.edcom/src/com/ed/edcom/Client.java b/io.openems.edge.katek.edcom/src/com/ed/edcom/Client.java index 4b04490507d..3e39681b187 100644 --- a/io.openems.edge.katek.edcom/src/com/ed/edcom/Client.java +++ b/io.openems.edge.katek.edcom/src/com/ed/edcom/Client.java @@ -196,7 +196,7 @@ public byte getAccessFeedb() { /** * Check if ID is accepted by inverter * - * @return true - ID accepted, false - ID not accpeted. + * @return true - ID accepted, false - ID not accepted. */ public boolean isIdAccepted() { @@ -206,7 +206,7 @@ public boolean isIdAccepted() { /** * Check if user password is accepted by inverter * - * @return true - ID accepted, false - ID not accpeted. + * @return true - ID accepted, false - ID not accepted. */ public boolean isPasswordAccepted() { return this.accessBitTest(2); diff --git a/io.openems.edge.simulator/bnd.bnd b/io.openems.edge.simulator/bnd.bnd index dbc46e64278..f674ba8c3a9 100644 --- a/io.openems.edge.simulator/bnd.bnd +++ b/io.openems.edge.simulator/bnd.bnd @@ -13,6 +13,7 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.evcs.api,\ io.openems.edge.io.api,\ io.openems.edge.meter.api,\ + io.openems.edge.predictor.api,\ io.openems.edge.pvinverter.api,\ io.openems.edge.thermometer.api,\ io.openems.edge.timedata.api,\ diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/DataContainer.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/DataContainer.java index b0bf32fbe4c..02b08f55252 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/DataContainer.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/DataContainer.java @@ -53,6 +53,22 @@ public Float[] getCurrentRecord() { return this.records.get(this.currentIndex); } + /** + * Gets all values for the key. If no keys exist, get the value of all records. + * + * @param key the Channel-Id + * @return the record values + */ + public List getValues(String key) { + var index = this.getIndex(key); + if (index == null) { + return List.of(); + } + return this.records.stream() // + .map(a -> a[index]) // + .toList(); + } + /** * Gets the value for the key from the current record. If no keys exist, get the * first value of the record. @@ -61,16 +77,9 @@ public Float[] getCurrentRecord() { * @return the record value */ public Optional getValue(String key) { - Integer index; - if (this.keys.isEmpty()) { - // no keys -> first value - index = 0; - } else { - // find index of key - index = this.keys.get(key); - if (index == null) { - return Optional.empty(); - } + var index = this.getIndex(key); + if (index == null) { + return Optional.empty(); } var record = this.getCurrentRecord(); if (index < record.length) { @@ -79,6 +88,22 @@ var record = this.getCurrentRecord(); return Optional.empty(); } + /** + * Gets the index of the kex. + * + * @param key the Channel-Id + * @return the index; possibly null + */ + private Integer getIndex(String key) { + if (this.keys.isEmpty()) { + // no keys -> first value + return 0; + } else { + // find index of key + return this.keys.get(key); + } + } + /** * Switch to the next row of values. */ diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorAppImpl.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorAppImpl.java index a1bce438a71..e646771b004 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorAppImpl.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorAppImpl.java @@ -473,6 +473,12 @@ public int getTimeDelta() { return -1; } + @Override + public List getValues(OpenemsType type, ChannelAddress channelAddress) { + // TODO Auto-generated method stub + return List.of(); + } + @Override public T getValue(OpenemsType type, ChannelAddress channelAddress) { if (this.currentSimulation == null) { diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/datasource/api/AbstractCsvDatasource.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/datasource/api/AbstractCsvDatasource.java index 82997c7a8c2..33aae79cab5 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/datasource/api/AbstractCsvDatasource.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/datasource/api/AbstractCsvDatasource.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.time.Duration; import java.time.LocalDateTime; +import java.util.List; import java.util.Set; import org.osgi.service.component.ComponentContext; @@ -59,6 +60,20 @@ public void handleEvent(Event event) { } } + @SuppressWarnings("unchecked") + @Override + public List getValues(OpenemsType type, ChannelAddress channelAddress) { + // First: try full ChannelAddress + var values = this.data.getValues(channelAddress.toString()); + if (values.isEmpty()) { + // Not found: try Channel-ID only (without Component-ID) + values = this.data.getValues(channelAddress.getChannelId()); + } + return values.stream() // + .map(v -> (T) TypeUtils.getAsType(type, v)) // + .toList(); + } + @Override public T getValue(OpenemsType type, ChannelAddress channelAddress) { // First: try full ChannelAddress diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/datasource/api/SimulatorDatasource.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/datasource/api/SimulatorDatasource.java index cac3c09485d..f5d1411d896 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/datasource/api/SimulatorDatasource.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/datasource/api/SimulatorDatasource.java @@ -1,5 +1,6 @@ package io.openems.edge.simulator.datasource.api; +import java.util.List; import java.util.Set; import io.openems.common.types.ChannelAddress; @@ -12,14 +13,24 @@ public interface SimulatorDatasource { * * @return the Channel-Id */ - Set getKeys(); + public Set getKeys(); /** * Returns the delta between two values in seconds. * * @return the delta in seconds */ - int getTimeDelta(); + public int getTimeDelta(); + + /** + * Gets all values for the given key (channelId) in the given type. + * + * @param the type + * @param type the expected type + * @param channelAddress the Channel-Address + * @return the values, possibly empty + */ + public List getValues(OpenemsType type, ChannelAddress channelAddress); /** * Gets the value for the given key (channelId) in the given type. @@ -29,6 +40,6 @@ public interface SimulatorDatasource { * @param channelAddress the Channel-Address * @return the value, possibly null */ - T getValue(OpenemsType type, ChannelAddress channelAddress); + public T getValue(OpenemsType type, ChannelAddress channelAddress); } diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/predictor/Config.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/predictor/Config.java new file mode 100644 index 00000000000..3498f4b97cb --- /dev/null +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/predictor/Config.java @@ -0,0 +1,38 @@ +package io.openems.edge.simulator.predictor; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +import io.openems.edge.predictor.api.prediction.LogVerbosity; + +@ObjectClassDefinition(// + name = "Simulator Predictor", // + description = "This Predictor simulates predictions from a DataSource") +@interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "timedata0"; + + @AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID") + String alias() default ""; + + @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?") + boolean enabled() default true; + + @AttributeDefinition(name = "Channel-Addresses", description = "List of Channel-Addresses this Predictor is used for, e.g. '*/ActivePower', '*/ActualPower'") + String[] channelAddresses() default { // + "_sum/ProductionActivePower", // + "_sum/UnmanagedConsumptionActivePower", // + "_sum/ConsumptionActivePower" }; + + @AttributeDefinition(name = "Datasource-ID", description = "ID of Simulator Datasource.") + String datasource_id() default "datasource0"; + + @AttributeDefinition(name = "Datasource target filter", description = "This is auto-generated by 'Datasource-ID'.") + String datasource_target() default "(enabled=true)"; + + @AttributeDefinition(name = "Log-Verbosity", description = "The log verbosity.") + LogVerbosity logVerbosity() default LogVerbosity.NONE; + + String webconsole_configurationFactory_nameHint() default "Simulator Predictor [{id}]"; +} \ No newline at end of file diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/predictor/SimulatorPredictor.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/predictor/SimulatorPredictor.java new file mode 100644 index 00000000000..0a80bff172b --- /dev/null +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/predictor/SimulatorPredictor.java @@ -0,0 +1,23 @@ +package io.openems.edge.simulator.predictor; + +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.predictor.api.prediction.Predictor; + +public interface SimulatorPredictor extends Predictor, OpenemsComponent { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + ; + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } +} diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/predictor/SimulatorPredictorImpl.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/predictor/SimulatorPredictorImpl.java new file mode 100644 index 00000000000..59fc9590f53 --- /dev/null +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/predictor/SimulatorPredictorImpl.java @@ -0,0 +1,102 @@ +package io.openems.edge.simulator.predictor; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.LinkedList; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.event.propertytypes.EventTopics; +import org.osgi.service.metatype.annotations.Designate; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.ChannelAddress; +import io.openems.common.types.OpenemsType; +import io.openems.edge.common.component.ClockProvider; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.event.EdgeEventConstants; +import io.openems.edge.common.type.TypeUtils; +import io.openems.edge.predictor.api.prediction.AbstractPredictor; +import io.openems.edge.predictor.api.prediction.Prediction; +import io.openems.edge.predictor.api.prediction.Predictor; +import io.openems.edge.simulator.datasource.api.SimulatorDatasource; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Simulator.Predictor", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +@EventTopics({ // + EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE // +}) +public class SimulatorPredictorImpl extends AbstractPredictor + implements SimulatorPredictor, Predictor, OpenemsComponent { + + @Reference + private ComponentManager componentManager; + + @Reference + private ConfigurationAdmin cm; + + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) + private SimulatorDatasource datasource; + + public SimulatorPredictorImpl() { + super(// + OpenemsComponent.ChannelId.values(), // + SimulatorPredictor.ChannelId.values() // + ); + } + + @Activate + private void activate(ComponentContext context, Config config) throws OpenemsNamedException { + super.activate(context, config.id(), config.alias(), config.enabled(), config.channelAddresses(), + config.logVerbosity()); + + // update filter for 'datasource' + if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "datasource", config.datasource_id())) { + return; + } + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + protected ClockProvider getClockProvider() { + return this.componentManager; + } + + @Override + protected Prediction createNewPrediction(ChannelAddress channelAddress) { + var source = this.datasource.getValues(OpenemsType.INTEGER, channelAddress); + if (source.isEmpty()) { + return Prediction.EMPTY_PREDICTION; + } + // Fill 48 hours starting from midnight; assume Datasource provides one vale per + // 5 minutes + var values = new Integer[48 /* hours */ * 4 /* quarters per hour */]; + var cache = new LinkedList(); + for (var i = 0; i < values.length; i++) { + while (cache.size() < 3) { + cache.addAll(source); + } + values[i] = TypeUtils.averageInt(cache.poll(), cache.poll(), cache.poll()); + } + var today = ZonedDateTime.now(this.componentManager.getClock()).truncatedTo(ChronoUnit.DAYS); + return Prediction.from(today, values); + } +} diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/datasource/csv/direct/SimulatorDatasourceCsvDirectImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/datasource/csv/direct/SimulatorDatasourceCsvDirectImplTest.java index 22f4942d5de..347a36a4535 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/datasource/csv/direct/SimulatorDatasourceCsvDirectImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/datasource/csv/direct/SimulatorDatasourceCsvDirectImplTest.java @@ -11,17 +11,33 @@ public class SimulatorDatasourceCsvDirectImplTest { private static final String COMPONENT_ID = "datasource0"; - @Test - public void test() throws Exception { - new ComponentTest(new SimulatorDatasourceCsvDirectImpl()) // + private static ComponentTest createTest(String componentId, String source) throws Exception { + return new ComponentTest(new SimulatorDatasourceCsvDirectImpl()) // .addReference("componentManager", new DummyComponentManager()) // .activate(MyConfig.create() // - .setId(COMPONENT_ID) // + .setId(componentId) // .setFactor(1) // .setFormat(CsvFormat.ENGLISH) // - .setSource("") // + .setSource(source) // .setTimeDelta(0) // - .build()) // + .build()); // + } + + /** + * Creates and activates a {@link SimulatorDatasourceCsvDirectImpl}. + * + * @param componentId the Component-ID + * @param source the data + * @return a {@link SimulatorDatasourceCsvDirectImpl} object + * @throws Exception on error + */ + public static SimulatorDatasourceCsvDirectImpl create(String componentId, String source) throws Exception { + return (SimulatorDatasourceCsvDirectImpl) createTest(componentId, source).getSut(); + } + + @Test + public void test() throws Exception { + createTest(COMPONENT_ID, "") // .next(new TestCase()) // ; } diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/predictor/MyConfig.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/predictor/MyConfig.java new file mode 100644 index 00000000000..4d2583743ae --- /dev/null +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/predictor/MyConfig.java @@ -0,0 +1,80 @@ +package io.openems.edge.simulator.predictor; + +import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.utils.ConfigUtils; +import io.openems.edge.predictor.api.prediction.LogVerbosity; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + protected static class Builder { + private String id; + private String datasourceId; + private String[] channelAddresses; + private LogVerbosity logVerbosity; + + private Builder() { + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setDatasourceId(String datasourceId) { + this.datasourceId = datasourceId; + return this; + } + + public Builder setChannelAddresses(String... channelAddresses) { + this.channelAddresses = channelAddresses; + return this; + } + + public Builder setLogVerbosity(LogVerbosity logVerbosity) { + this.logVerbosity = logVerbosity; + return this; + } + + public MyConfig build() { + return new MyConfig(this); + } + } + + /** + * Create a Config builder. + * + * @return a {@link Builder} + */ + public static Builder create() { + return new Builder(); + } + + private final Builder builder; + + private MyConfig(Builder builder) { + super(Config.class, builder.id); + this.builder = builder; + } + + @Override + public String[] channelAddresses() { + return this.builder.channelAddresses; + } + + @Override + public String datasource_id() { + return this.builder.datasourceId; + } + + @Override + public String datasource_target() { + return ConfigUtils.generateReferenceTargetFilter(this.id(), this.datasource_id()); + } + + @Override + public LogVerbosity logVerbosity() { + return this.builder.logVerbosity; + } + +} \ No newline at end of file diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/predictor/SimulatorPredictorImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/predictor/SimulatorPredictorImplTest.java new file mode 100644 index 00000000000..cb52b93658c --- /dev/null +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/predictor/SimulatorPredictorImplTest.java @@ -0,0 +1,55 @@ +package io.openems.edge.simulator.predictor; + +import static org.junit.Assert.assertEquals; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import org.junit.Test; + +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.test.TimeLeapClock; +import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.test.ComponentTest; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.predictor.api.prediction.LogVerbosity; +import io.openems.edge.simulator.datasource.csv.direct.SimulatorDatasourceCsvDirectImplTest; + +public class SimulatorPredictorImplTest { + + private static final TimeLeapClock CLOCK = new TimeLeapClock(Instant.ofEpochSecond(946684800), ZoneId.of("UTC")); + private static final String COMPONENT_ID = "predictor0"; + private static final String DATASOURCE_ID = "datasource0"; + private static final ChannelAddress SUM_PRODUCTION = new ChannelAddress("_sum", "ProductionActivePower"); + + @Test + public void test() throws OpenemsException, Exception { + final var datasource = SimulatorDatasourceCsvDirectImplTest.create(DATASOURCE_ID, """ + 10 + 20 + 30 + 40 + """); + final var sut = new SimulatorPredictorImpl(); + new ComponentTest(sut) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("datasource", datasource) // + .addReference("componentManager", new DummyComponentManager(CLOCK)) // + .activate(MyConfig.create() // + .setId(COMPONENT_ID) // + .setDatasourceId(DATASOURCE_ID) // + .setChannelAddresses(SUM_PRODUCTION.toString()) // + .setLogVerbosity(LogVerbosity.REQUESTED_PREDICTIONS) // + .build()); // + + var p = sut.createNewPrediction(SUM_PRODUCTION); + assertEquals(192, p.asArray().length); + assertEquals(Integer.valueOf(20), p.asArray()[0]); + assertEquals(Integer.valueOf(23), p.asArray()[1]); + assertEquals(Integer.valueOf(27), p.asArray()[2]); + assertEquals(ZonedDateTime.of(2000, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC")), p.valuePerQuarter.firstKey()); + } + +} diff --git a/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jReadHandler.java b/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jReadHandler.java index 7ee00663374..e12637892de 100644 --- a/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jReadHandler.java +++ b/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jReadHandler.java @@ -150,7 +150,7 @@ private static Timeranges getTimerangesOfNotSendData(RrdDb db, long start) throw * @param rrdDbId the id of the rrdb * @param notSendChannel the channel with the timestamps where the data got * not send - * @param lastResendTimestamp the timstamp of the last resend + * @param lastResendTimestamp the timestamp of the last resend * @param debugMode if debugMode is active * @return the {@link Timeranges} * @throws OpenemsNamedException on error diff --git a/io.openems.wrapper/bnd.bnd b/io.openems.wrapper/bnd.bnd index 8bed7415c14..d024f94a589 100644 --- a/io.openems.wrapper/bnd.bnd +++ b/io.openems.wrapper/bnd.bnd @@ -24,7 +24,7 @@ Bundle-Description: This wraps external java libraries that do not have OSGi hea com.google.gson;version='2.10.1',\ de.bytefish:pgbulkinsert;version='8.1.4',\ fr.turri:aXMLRPC;version='1.13.0',\ - org.dhatim:fastexcel;version='0.18.1',\ - org.dhatim:fastexcel-reader;version='0.18.1',\ + org.dhatim:fastexcel;version='0.18.4',\ + org.dhatim:fastexcel-reader;version='0.18.4',\ org.eclipse.paho.mqttv5.client;version='1.2.5',\ - org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm;version='1.7.3',\ + org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm;version='1.9.0',\ diff --git a/io.openems.wrapper/fastexcel.bnd b/io.openems.wrapper/fastexcel.bnd index e3fa142345c..68c2302413d 100644 --- a/io.openems.wrapper/fastexcel.bnd +++ b/io.openems.wrapper/fastexcel.bnd @@ -1,11 +1,11 @@ Bundle-Name: fastexcel Bundle-DocURL: https://github.com/dhatim/fastexcel Bundle-License: https://opensource.org/licenses/Apache-2.0 -Bundle-Version: 0.18.1 +Bundle-Version: 0.18.4 Include-Resource: \ - @fastexcel-0.18.1.jar,\ - @fastexcel-reader-0.18.1.jar,\ + @fastexcel-0.18.4.jar,\ + @fastexcel-reader-0.18.4.jar,\ -dsannotations: * diff --git a/io.openems.wrapper/kotlinx-coroutines-core-jvm.bnd b/io.openems.wrapper/kotlinx-coroutines-core-jvm.bnd index eaafb47be8d..3b276e9c952 100644 --- a/io.openems.wrapper/kotlinx-coroutines-core-jvm.bnd +++ b/io.openems.wrapper/kotlinx-coroutines-core-jvm.bnd @@ -2,10 +2,10 @@ Bundle-Name: kotlinx-coroutines-core-jvm Bundle-Description: The Java InfluxDB 2.0 Client Core Bundle-DocURL: https://github.com/influxdata/influxdb-client-client Bundle-License: https://opensource.org/licenses/MIT -Bundle-Version: 1.8.1 +Bundle-Version: 1.9.0 Include-Resource: \ - @kotlinx-coroutines-core-jvm-1.8.1.jar,\ + @kotlinx-coroutines-core-jvm-1.9.0.jar,\ Export-Package: \ kotlinx.coroutines,\ diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json index c3095af0cf3..8a3902ecddb 100644 --- a/ui/.eslintrc.json +++ b/ui/.eslintrc.json @@ -89,8 +89,8 @@ } ], "@typescript-eslint/member-ordering": "error", - "no-multiple-empty-lines": "error", - "quotes": [ + "@stylistic/no-multiple-empty-lines": "error", + "@stylistic/quotes": [ "error", "double" ], diff --git a/ui/package-lock.json b/ui/package-lock.json index edc7956ab67..fca2ca8fb74 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,35 +1,35 @@ { "name": "openems-ui", - "version": "2024.9.0", + "version": "2024.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openems-ui", - "version": "2024.9.0", + "version": "2024.10.0", "license": "AGPL-3.0", "dependencies": { - "@angular/animations": "18.0.5", - "@angular/common": "18.0.5", - "@angular/core": "18.0.5", - "@angular/forms": "18.0.5", - "@angular/platform-browser": "18.0.5", - "@angular/platform-browser-dynamic": "18.0.5", - "@angular/router": "18.0.5", - "@angular/service-worker": "18.0.5", - "@capacitor-community/file-opener": "^6.0.0", - "@capacitor/android": "^6.0.0", - "@capacitor/app": "^6.0.0", - "@capacitor/core": "^6.0.0", - "@capacitor/filesystem": "^6.0.0", - "@capacitor/ios": "^6.0.0", - "@capacitor/splash-screen": "^6.0.0", + "@angular/animations": "18.2.5", + "@angular/common": "18.2.5", + "@angular/core": "18.2.5", + "@angular/forms": "18.2.5", + "@angular/platform-browser": "18.2.5", + "@angular/platform-browser-dynamic": "18.2.5", + "@angular/router": "18.2.5", + "@angular/service-worker": "18.2.5", + "@capacitor-community/file-opener": "^6.0.1", + "@capacitor/android": "^6.1.2", + "@capacitor/app": "^6.0.1", + "@capacitor/core": "^6.1.2", + "@capacitor/filesystem": "^6.0.1", + "@capacitor/ios": "^6.1.2", + "@capacitor/splash-screen": "^6.0.2", "@ionic-native/core": "^5.36.0", "@ionic-native/file-opener": "^5.36.0", "@ionic/angular": "^6.7.5", - "@ngx-formly/core": "^6.3.0", - "@ngx-formly/ionic": "^6.3.6", - "@ngx-formly/schematics": "^6.3.0", + "@ngx-formly/core": "^6.3.7", + "@ngx-formly/ionic": "^6.3.7", + "@ngx-formly/schematics": "^6.3.7", "@ngx-translate/core": "^15.0.0", "@nodro7/angular-mydatepicker": "^0.14.0", "capacitor-blob-writer": "^1.1.17", @@ -51,47 +51,47 @@ "ngx-spinner": "^16.0.2", "roboto-fontface": "^0.10.0", "rxjs": "~6.6.7", - "swiper": "11.1.11", + "swiper": "11.1.14", "tslib": "^2.6.2", "uuid": "^10.0.0", "zone.js": "~0.14.7" }, "devDependencies": { - "@angular-devkit/build-angular": "^18.0.5", - "@angular-devkit/core": "18.0.5", - "@angular-devkit/schematics": "18.0.5", - "@angular-eslint/builder": "^18.1.0", - "@angular-eslint/eslint-plugin": "^18.1.0", - "@angular-eslint/eslint-plugin-template": "^18.1.0", - "@angular-eslint/template-parser": "^18.1.0", - "@angular/cli": "18.1.0", - "@angular/compiler": "18.0.5", - "@angular/compiler-cli": "18.0.5", - "@angular/language-service": "18.0.5", + "@angular-devkit/build-angular": "^18.2.5", + "@angular-devkit/core": "18.2.5", + "@angular-devkit/schematics": "18.2.5", + "@angular-eslint/builder": "^18.3.1", + "@angular-eslint/eslint-plugin": "^18.3.1", + "@angular-eslint/eslint-plugin-template": "^18.3.1", + "@angular-eslint/template-parser": "^18.3.1", + "@angular/cli": "18.2.5", + "@angular/compiler": "18.2.5", + "@angular/compiler-cli": "18.2.5", + "@angular/language-service": "18.2.5", "@capacitor/assets": "^3.0.5", "@capacitor/cli": "6.1.2", "@ionic/angular-toolkit": "^11.0.1", "@ionic/cli": "^7.2.0", - "@stylistic/eslint-plugin": "^2.7.2", + "@stylistic/eslint-plugin": "^2.8.0", "@types/jasmine": "~4.3.6", "@types/jasminewd2": "~2.0.13", "@types/json-schema": "^7.0.15", "@types/node": "^20.12.6", - "@types/qs": "^6.9.15", + "@types/qs": "^6.9.16", "@types/range-parser": "^1.2.7", "@types/send": "^0.17.4", "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", - "@typescript-eslint/types": "^7.0.0", + "@typescript-eslint/types": "^8.7.0", "eslint": "^8.57.0", - "eslint-plugin-import": "2.29.1", - "eslint-plugin-jsdoc": "48.10.0", + "eslint-plugin-import": "2.30.0", + "eslint-plugin-jsdoc": "50.2.4", "eslint-plugin-prefer-arrow": "1.2.3", - "eslint-plugin-unused-imports": "^4.1.3", - "jasmine-core": "~4.5.0", + "eslint-plugin-unused-imports": "^4.1.4", + "jasmine-core": "~5.3.0", "jasmine-spec-reporter": "~7.0.0", - "karma": "~6.4.2", + "karma": "~6.4.4", "karma-chrome-launcher": "~3.2.0", "karma-coverage": "~2.2.1", "karma-coverage-istanbul-reporter": "~3.0.3", @@ -118,13 +118,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.2.tgz", - "integrity": "sha512-LPRl9jhcf0NgshaL6RoUy1uL/cAyNt7oxctoZ9EHUu8eh5E9W/jZGhVowjOLpirwqYhmEzKJJIeS49Ssqs3RQg==", + "version": "0.1802.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.5.tgz", + "integrity": "sha512-c7sVoW85Yqj7IYvNKxtNSGS5I7gWpORorg/xxLZX3OkHWXDrwYbb5LN/2p5/Aytxyb0aXl4o5fFOu6CUwcaLUw==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.2", + "@angular-devkit/core": "18.2.5", "rxjs": "7.8.1" }, "engines": { @@ -133,73 +132,27 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/architect/node_modules/@angular-devkit/core": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.2.tgz", - "integrity": "sha512-Zz0tGptI/QQnUBDdp+1G5wGwQWMjpfe2oO+UohkrDVgFS71yVj4VDnOy51kMTxBvzw+36evTgthPpmzqPIfxBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.17.1", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.2", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/architect/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/@angular-devkit/architect/node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@angular-devkit/build-angular": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.2.tgz", - "integrity": "sha512-7HEnTN2T1jnjuItXKcApOsoYGgfou4+POju3ZbwIQukDZ3B2COskvQkVTxqPNrQ0ZjT2mxZYoVlmGW9M+7N25g==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.6.tgz", + "integrity": "sha512-u12cJZttgs5j7gICHWSmcaTCu0EFXEzKqI8nkYCwq2MtuJlAXiMQSXYuEP9OU3Go4vMAPtQh2kShyOWCX5b4EQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.2", - "@angular-devkit/build-webpack": "0.1802.2", - "@angular-devkit/core": "18.2.2", - "@angular/build": "18.2.2", + "@angular-devkit/architect": "0.1802.6", + "@angular-devkit/build-webpack": "0.1802.6", + "@angular-devkit/core": "18.2.6", + "@angular/build": "18.2.6", "@babel/core": "7.25.2", "@babel/generator": "7.25.0", "@babel/helper-annotate-as-pure": "7.24.7", @@ -210,7 +163,7 @@ "@babel/preset-env": "7.25.3", "@babel/runtime": "7.25.0", "@discoveryjs/json-ext": "0.6.1", - "@ngtools/webpack": "18.2.2", + "@ngtools/webpack": "18.2.6", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -250,10 +203,10 @@ "terser": "5.31.6", "tree-kill": "1.2.2", "tslib": "2.6.3", - "vite": "5.4.0", + "vite": "5.4.6", "watchpack": "2.4.1", "webpack": "5.94.0", - "webpack-dev-middleware": "7.3.0", + "webpack-dev-middleware": "7.4.2", "webpack-dev-server": "5.0.4", "webpack-merge": "6.0.1", "webpack-subresource-integrity": "5.1.0" @@ -317,10 +270,26 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/architect": { + "version": "0.1802.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.6.tgz", + "integrity": "sha512-oF7cPFdTLxeuvXkK/opSdIxZ1E4LrBbmuytQ/nCoAGOaKBWdqvwagRZ6jVhaI0Gwu48rkcV7Zhesg/ESNnROdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "18.2.6", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.2.tgz", - "integrity": "sha512-Zz0tGptI/QQnUBDdp+1G5wGwQWMjpfe2oO+UohkrDVgFS71yVj4VDnOy51kMTxBvzw+36evTgthPpmzqPIfxBw==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.6.tgz", + "integrity": "sha512-la4CFvs5PcRWSkQ/H7TB5cPZirFVA9GoWk5LzIk8si6VjWBJRm8b3keKJoC9LlNeABRUIR5z0ocYkyQQUhdMfg==", "dev": true, "license": "MIT", "dependencies": { @@ -346,14 +315,14 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@angular/build": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.2.tgz", - "integrity": "sha512-okaDdTMXnDhvnnnih6rPQnexL6htfEAPr19bB1Ci9d31gEjVuKZCjlcw2sPZ6BUyilwC9nZlCI5vbH1Ljf6mzA==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.6.tgz", + "integrity": "sha512-TQzX6Mi7uXFvmz7+OVl4Za7WawYPcx+B5Ewm6IY/DdMyB9P/Z4tbKb1LO+ynWUXYwm7avXo6XQQ4m5ArDY5F/A==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.2", + "@angular-devkit/architect": "0.1802.6", "@babel/core": "7.25.2", "@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7", @@ -372,10 +341,10 @@ "parse5-html-rewriting-stream": "7.0.0", "picomatch": "4.0.2", "piscina": "4.6.1", - "rollup": "4.20.0", + "rollup": "4.22.4", "sass": "1.77.6", "semver": "7.6.3", - "vite": "5.4.0", + "vite": "5.4.6", "watchpack": "2.4.1" }, "engines": { @@ -415,9 +384,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz", - "integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", "cpu": [ "arm" ], @@ -429,9 +398,9 @@ ] }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-android-arm64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz", - "integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", "cpu": [ "arm64" ], @@ -443,9 +412,9 @@ ] }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz", - "integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", "cpu": [ "arm64" ], @@ -457,9 +426,9 @@ ] }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-darwin-x64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz", - "integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", "cpu": [ "x64" ], @@ -471,9 +440,9 @@ ] }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz", - "integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", "cpu": [ "arm" ], @@ -485,9 +454,9 @@ ] }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz", - "integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", "cpu": [ "arm" ], @@ -499,9 +468,9 @@ ] }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz", - "integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", "cpu": [ "arm64" ], @@ -513,9 +482,9 @@ ] }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz", - "integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", "cpu": [ "arm64" ], @@ -527,9 +496,9 @@ ] }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz", - "integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", "cpu": [ "ppc64" ], @@ -541,9 +510,9 @@ ] }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz", - "integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", "cpu": [ "riscv64" ], @@ -555,9 +524,9 @@ ] }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz", - "integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", "cpu": [ "s390x" ], @@ -569,9 +538,9 @@ ] }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz", - "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", "cpu": [ "x64" ], @@ -583,9 +552,9 @@ ] }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz", - "integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", "cpu": [ "x64" ], @@ -597,9 +566,9 @@ ] }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz", - "integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", "cpu": [ "arm64" ], @@ -611,9 +580,9 @@ ] }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz", - "integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", "cpu": [ "ia32" ], @@ -625,9 +594,9 @@ ] }, "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz", - "integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", "cpu": [ "x64" ], @@ -638,85 +607,17 @@ "win32" ] }, - "node_modules/@angular-devkit/build-angular/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular-devkit/build-angular/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "node_modules/@angular-devkit/build-angular/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true, "license": "MIT" }, - "node_modules/@angular-devkit/build-angular/node_modules/listr2": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", - "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^4.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/rollup": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz", - "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dev": true, "license": "MIT", "dependencies": { @@ -730,22 +631,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.20.0", - "@rollup/rollup-android-arm64": "4.20.0", - "@rollup/rollup-darwin-arm64": "4.20.0", - "@rollup/rollup-darwin-x64": "4.20.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.20.0", - "@rollup/rollup-linux-arm-musleabihf": "4.20.0", - "@rollup/rollup-linux-arm64-gnu": "4.20.0", - "@rollup/rollup-linux-arm64-musl": "4.20.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.20.0", - "@rollup/rollup-linux-riscv64-gnu": "4.20.0", - "@rollup/rollup-linux-s390x-gnu": "4.20.0", - "@rollup/rollup-linux-x64-gnu": "4.20.0", - "@rollup/rollup-linux-x64-musl": "4.20.0", - "@rollup/rollup-win32-arm64-msvc": "4.20.0", - "@rollup/rollup-win32-ia32-msvc": "4.20.0", - "@rollup/rollup-win32-x64-msvc": "4.20.0", + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", "fsevents": "~2.3.2" } }, @@ -754,79 +655,65 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true }, - "node_modules/@angular-devkit/build-angular/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1802.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.6.tgz", + "integrity": "sha512-JMLcXFaitJplwZMKkqhbYirINCRD6eOPZuIGaIOVynXYGWgvJkLT9t5C2wm9HqSLtp1K7NcYG2Y7PtTVR4krnQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "@angular-devkit/architect": "0.1802.6", + "rxjs": "7.8.1" }, "engines": { - "node": ">=12" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^5.0.2" } }, - "node_modules/@angular-devkit/build-angular/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@angular-devkit/build-angular/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/architect": { + "version": "0.1802.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.6.tgz", + "integrity": "sha512-oF7cPFdTLxeuvXkK/opSdIxZ1E4LrBbmuytQ/nCoAGOaKBWdqvwagRZ6jVhaI0Gwu48rkcV7Zhesg/ESNnROdw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "@angular-devkit/core": "18.2.6", + "rxjs": "7.8.1" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/build-webpack": { - "version": "0.1802.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.2.tgz", - "integrity": "sha512-Pj+YmKh0nJOKl6QAsqYh3SqfuVJrFqjyp5WrG9BgfsMD9GCMD+5teMHNYJlp+vG/C8e7VdZp4rqOon8K9Xn4Mw==", + "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/core": { + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.6.tgz", + "integrity": "sha512-la4CFvs5PcRWSkQ/H7TB5cPZirFVA9GoWk5LzIk8si6VjWBJRm8b3keKJoC9LlNeABRUIR5z0ocYkyQQUhdMfg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.2", - "rxjs": "7.8.1" + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" }, "engines": { "node": "^18.19.1 || ^20.11.1 || >=22.0.0", @@ -834,8 +721,12 @@ "yarn": ">= 1.13.0" }, "peerDependencies": { - "webpack": "^5.30.0", - "webpack-dev-server": "^5.0.2" + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { @@ -849,15 +740,14 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.0.5.tgz", - "integrity": "sha512-sGtrS0SqkcBvyuv0QkIfyadwPgDhMroz1r51lMh1hwzJaJ0LNuVMLviEeYIybeBnvAdp9YvYC8I1WgB/FUEFBw==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.5.tgz", + "integrity": "sha512-r9TumPlJ8PvA2+yz4sp+bUHgtznaVKzhvXTN5qL1k4YP8LJ7iZWMR2FOP+HjukHZOTsenzmV9pszbogabqwoZQ==", "dev": true, - "license": "MIT", "dependencies": { - "ajv": "8.13.0", + "ajv": "8.17.1", "ajv-formats": "3.0.1", - "jsonc-parser": "3.2.1", + "jsonc-parser": "3.3.1", "picomatch": "4.0.2", "rxjs": "7.8.1", "source-map": "0.7.4" @@ -876,13 +766,6 @@ } } }, - "node_modules/@angular-devkit/core/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, - "license": "MIT" - }, "node_modules/@angular-devkit/core/node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -894,15 +777,14 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.0.5.tgz", - "integrity": "sha512-hZwAq3hwuJzCuh7uqO/7T9IMERhYVxz+ganJlEykpyr58o0IjUM1Q4ZSH5UOYlGRPdBCZJbfiafZ0Sg5w5xBww==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.5.tgz", + "integrity": "sha512-NUmz2UQ1Xl4cf4j1AgkwIfsCjBzAPgfeC3IBrD29hSOBE1Y3j6auqjBkvw50v6mbSPxESND995Xy13HpK1Xflw==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.0.5", - "jsonc-parser": "3.2.1", - "magic-string": "0.30.10", + "@angular-devkit/core": "18.2.5", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.11", "ora": "5.4.1", "rxjs": "7.8.1" }, @@ -912,23 +794,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/schematics/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, - "license": "MIT" - }, - "node_modules/@angular-devkit/schematics/node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - }, "node_modules/@angular-devkit/schematics/node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -940,9 +805,9 @@ } }, "node_modules/@angular-eslint/builder": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-18.3.0.tgz", - "integrity": "sha512-httEQyqyBw3+0CRtAa7muFxHrauRfkEfk/jmrh5fn2Eiu+I53hAqFPgrwVi1V6AP/kj2zbAiWhd5xM3pMJdoRQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-18.3.1.tgz", + "integrity": "sha512-cPc7Ye9zDs5M4i+feL6vob+mh7yX5vxvOS5KQIhneUrp5e9D+IGuNFMmBLlOPpmklSc9XJBtuvI5Zjuh4z1ETw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -951,21 +816,21 @@ } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.3.0.tgz", - "integrity": "sha512-v/59FxUKnMzymVce99gV43huxoqXWMb85aKvzlNvLN+ScDu6ZE4YMiTQNpfapVL2lkxhs0uwB3jH17EYd5TcsA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.3.1.tgz", + "integrity": "sha512-sikmkjfsXPpPTku1aQkQ1MNNEKGBgGGRvUN/WeNS9dhCJ4dxU3O7dZctt1aQWj+W3nbuUtDiimAWF5fZHGFE2Q==", "dev": true, "license": "MIT" }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-18.3.0.tgz", - "integrity": "sha512-Vl7gfPMXxvtHTjYdlzR161aj5xrqW6T57wd8ToQ7Gqzm0qHGfY6kE4SQobUa2LCYckTNSlv+zXe48C4ah/dSjw==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-18.3.1.tgz", + "integrity": "sha512-MP4Nm+SHboF8KdnN0KpPEGAaTTzDLPm3+S/4W3Mg8onqWCyadyd4mActh9mK/pvCj8TVlb/SW1zeTtdMYhwonw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.3.0", - "@angular-eslint/utils": "18.3.0" + "@angular-eslint/bundled-angular-compiler": "18.3.1", + "@angular-eslint/utils": "18.3.1" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -974,14 +839,14 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-18.3.0.tgz", - "integrity": "sha512-ddR/qwYbUeq9IpyVKrPbfZyRBTy6V8uc5I0JcBKttQ4CZ4joXhqsVgWFsI+JAMi8E66uNj1VC7NuKCOjDINv2Q==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-18.3.1.tgz", + "integrity": "sha512-hBJ3+f7VSidvrtYaXH7Vp0sWvblA9jLK2c6uQzhYGWdEDUcTg7g7VI9ThW39WvMbHqkyzNE4PPOynK69cBEDGg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.3.0", - "@angular-eslint/utils": "18.3.0", + "@angular-eslint/bundled-angular-compiler": "18.3.1", + "@angular-eslint/utils": "18.3.1", "aria-query": "5.3.0", "axobject-query": "4.1.0" }, @@ -992,13 +857,13 @@ } }, "node_modules/@angular-eslint/template-parser": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-18.3.0.tgz", - "integrity": "sha512-1mUquqcnugI4qsoxcYZKZ6WMi6RPelDcJZg2YqGyuaIuhWmi3ZqJZLErSSpjP60+TbYZu7wM8Kchqa1bwJtEaQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-18.3.1.tgz", + "integrity": "sha512-JUUkfWH1G+u/Uk85ZYvJSt/qwN/Ko+jlXFtzBEcknJZsTWTwBcp36v77gPZe5FmKSziJZpyPUd+7Kiy6tuSCTw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.3.0", + "@angular-eslint/bundled-angular-compiler": "18.3.1", "eslint-scope": "^8.0.2" }, "peerDependencies": { @@ -1007,13 +872,13 @@ } }, "node_modules/@angular-eslint/utils": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-18.3.0.tgz", - "integrity": "sha512-sCrkHkpxBJZLuCikdboZoawCfc2UgbJv+T14tu2uQCv+Vwzeadnu04vkeY2vTkA8GeBdBij/G9/N/nvwmwVw3g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-18.3.1.tgz", + "integrity": "sha512-sd9niZI7h9H2FQ7OLiQsLFBhjhRQTASh+Q0+4+hyjv9idbSHBJli8Gsi2fqj9zhtMKpAZFTrWzuLUpubJ9UYbA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.3.0" + "@angular-eslint/bundled-angular-compiler": "18.3.1" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -1022,9 +887,9 @@ } }, "node_modules/@angular/animations": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.0.5.tgz", - "integrity": "sha512-RYwlS+4I33beAWdzFFmaDPqXZN+r66qPzzMOk9LQguwF76eBJbykHniODalSLvjrY6Iz7CULavByYNpzq2TT7A==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.5.tgz", + "integrity": "sha512-IlXtW/Nj48ZzjHUzH1TykZcSR64ScJx39T3IHnjV2z/bVATzZ36JGoadQHdqpJNKBodYJNgtJCGLCbgAvGWY2g==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1033,7 +898,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.0.5" + "@angular/core": "18.2.5" } }, "node_modules/@angular/cdk": { @@ -1055,27 +920,27 @@ } }, "node_modules/@angular/cli": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.1.0.tgz", - "integrity": "sha512-2E+b7S/736AOmxf5je9OWoPpgPY240TfJfFXwQiVvq/4KyC+ZR9lBrqRx72Xghn8nu3z8Q2BPZIXVGZppl0USQ==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.5.tgz", + "integrity": "sha512-97uNs0HsOdnMaTlNJKFjIBUXw0wz43uYvSSKmIpBt7eq1LaPLju1G/qpDIHx2YwhMClPrXXrW2H/xdvqZiIw+w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1801.0", - "@angular-devkit/core": "18.1.0", - "@angular-devkit/schematics": "18.1.0", - "@inquirer/prompts": "5.0.7", - "@listr2/prompt-adapter-inquirer": "2.0.13", - "@schematics/angular": "18.1.0", + "@angular-devkit/architect": "0.1802.5", + "@angular-devkit/core": "18.2.5", + "@angular-devkit/schematics": "18.2.5", + "@inquirer/prompts": "5.3.8", + "@listr2/prompt-adapter-inquirer": "2.0.15", + "@schematics/angular": "18.2.5", "@yarnpkg/lockfile": "1.1.0", "ini": "4.1.3", "jsonc-parser": "3.3.1", - "listr2": "8.2.3", - "npm-package-arg": "11.0.2", - "npm-pick-manifest": "9.0.1", + "listr2": "8.2.4", + "npm-package-arg": "11.0.3", + "npm-pick-manifest": "9.1.0", "pacote": "18.0.6", "resolve": "1.22.8", - "semver": "7.6.2", + "semver": "7.6.3", "symbol-observable": "4.0.0", "yargs": "17.7.2" }, @@ -1088,163 +953,49 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { - "version": "0.1801.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1801.0.tgz", - "integrity": "sha512-iZa3J3CrZT6MKiHPw8ijgVwMyCMewCsP4xc75SetUwF/yuqRUHygALs5jJVZQFQjSFUrkg9gqXa1cCjFDwpT8A==", - "dev": true, - "license": "MIT", + "node_modules/@angular/common": { + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.5.tgz", + "integrity": "sha512-m+KJrtbFXTE36jP/po6UAMeUR/enQxRHpVGLCRcIcE7VWVH1ZcOvoW1yqh2A6k+KxWXeajlq/Z04nnMhcoxMRw==", "dependencies": { - "@angular-devkit/core": "18.1.0", - "rxjs": "7.8.1" + "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "18.2.5", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@angular/cli/node_modules/@angular-devkit/core": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.1.0.tgz", - "integrity": "sha512-6eXQDzHZCbpSMLv9Ohl+1QyLVDmGEXpuuHz3y64LfUTP0aEiBaxk96FjLXIxzJ4f2pbbW2XHzc+yuboGToRA0w==", - "dev": true, + "node_modules/@angular/compiler": { + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.5.tgz", + "integrity": "sha512-vcqe9x4dGGAnMfPhEpcZyiSVgAiqJeK80LqP1vWoAmBR+HeOqAilSv6SflcLAtuTzwgzMMAvD2T+SMCgUvaqww==", "license": "MIT", "dependencies": { - "ajv": "8.16.0", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.2", - "rxjs": "7.8.1", - "source-map": "0.7.4" + "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "chokidar": "^3.5.2" + "@angular/core": "18.2.5" }, "peerDependenciesMeta": { - "chokidar": { + "@angular/core": { "optional": true } } }, - "node_modules/@angular/cli/node_modules/@angular-devkit/schematics": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.1.0.tgz", - "integrity": "sha512-BjrYutLfYFiPOSEcLBWCj3ENkwDn8gMfBSJesaBz7OrZBZGK5j0dVgBLIsGTP96TKo4o4vszJQOvS4AtV6xMGg==", + "node_modules/@angular/compiler-cli": { + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.5.tgz", + "integrity": "sha512-CCCtZobUTUfId/RTYtuDCw5R1oK0w65hdAUMRP1MdGmd8bb8DKJA86u1QCWwozL3rbXlIIX4ognQ6urQ43k/Gw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.1.0", - "jsonc-parser": "3.3.1", - "magic-string": "0.30.10", - "ora": "5.4.1", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/cli/node_modules/ajv": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", - "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@angular/cli/node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - }, - "node_modules/@angular/cli/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@angular/cli/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@angular/common": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.0.5.tgz", - "integrity": "sha512-yItVQSu+Rx8gthWJDTOHwbzItY8/lqmmmYA1RMex0u3GkJoX3/3TZSGXbbBXl8GH8vmQOfp9yj3C02JmlwldRg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/core": "18.0.5", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/compiler": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.0.5.tgz", - "integrity": "sha512-U1/qjNDjxMukXwQrJZjmr87KVxQmHbD7fxVlg0+qafHLe+YDuCtyOfQSGEZrWhwktxvAYZbl3FK+m3Hnk/D3Nw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/core": "18.0.5" - }, - "peerDependenciesMeta": { - "@angular/core": { - "optional": true - } - } - }, - "node_modules/@angular/compiler-cli": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.0.5.tgz", - "integrity": "sha512-aFKDDTsRmc691EkNRj9OkrKNXDOaHdXB42MyUrj3WwJIJFMnSY/UDf6h+CRVF0U+CITszFyWhmeHQRA/3mJWNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "7.24.7", + "@babel/core": "7.25.2", "@jridgewell/sourcemap-codec": "^1.4.14", "chokidar": "^3.0.0", "convert-source-map": "^1.5.1", @@ -1262,62 +1013,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.0.5", - "typescript": ">=5.4 <5.5" - } - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", - "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helpers": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "@angular/compiler": "18.2.5", + "typescript": ">=5.4 <5.6" } }, "node_modules/@angular/core": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.0.5.tgz", - "integrity": "sha512-0UuL+aMMWGYksz09YBsiHq1li7GmL8obB3IC3T5MwDqnn7FGRUBfBUOZEkM6B+pwgg+RAtNdJkbCfbh1z74bFQ==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.5.tgz", + "integrity": "sha512-5BLVc5gXxzanQkADNS9WPsor3vNF5nQcyIHBi5VScErwM5vVZ7ATH1iZwaOg1ykDEVTFVhKDwD0X1aaqGDbhmQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1327,13 +1030,13 @@ }, "peerDependencies": { "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.14.0" + "zone.js": "~0.14.10" } }, "node_modules/@angular/forms": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.0.5.tgz", - "integrity": "sha512-nO7bN+nO2/czgKSvPx6ewqpfb8xXOyns06uovWpAXSH4jYoiZ6CHTHhOKrOL/3SRkhUV9u+EUXTTAOSBkS+OBA==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.5.tgz", + "integrity": "sha512-ohKeH+EZCCIyGSiFYlraWLzssGAZc13P92cuYpXB62322PkcA5u0IT72mML9JWGKRqF2zteVsw4koWHVxXM5mA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1342,16 +1045,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.0.5", - "@angular/core": "18.0.5", - "@angular/platform-browser": "18.0.5", + "@angular/common": "18.2.5", + "@angular/core": "18.2.5", + "@angular/platform-browser": "18.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.0.5.tgz", - "integrity": "sha512-ahZnsUk8q/4k+okP9hBcfWRiOiMximSAI7Vq5M/fe9cezykt8cWEzxgRoduTvDKoQPqcRl0nHlDYju2zkXcU6g==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.5.tgz", + "integrity": "sha512-JE6ck4UWXayiG8ptJJtkrKCjy+5Ftktgsoj4QGdQzMhbpia7Wge5XDj28o+bwEFndRnP6ihRtud63IvOz9aKFQ==", "dev": true, "license": "MIT", "engines": { @@ -1359,9 +1062,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.0.5.tgz", - "integrity": "sha512-hBKaGz7dhsjNhD0aWB8G2/YZQ/MaBhzFIQSAZMPs2ccAqH1Jx772/Y11k57seA3VaPpnL8WZ1apOSJgALUJ//w==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.5.tgz", + "integrity": "sha512-PoX9idwnOpTJBlujzZ2nFGOsmCnZzOH7uNSWIR7trdoq0b1AFXfrxlCQ36qWamk7bbhJI4H28L8YTmKew/nXDA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1370,9 +1073,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "18.0.5", - "@angular/common": "18.0.5", - "@angular/core": "18.0.5" + "@angular/animations": "18.2.5", + "@angular/common": "18.2.5", + "@angular/core": "18.2.5" }, "peerDependenciesMeta": { "@angular/animations": { @@ -1381,9 +1084,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.0.5.tgz", - "integrity": "sha512-i8CXojKcjsKzD2JR2clIisqavlHCW1jw+F2hJVrf/JR9iu6kVpGpZOqb3yYHoQCsPa7hUzQnn0ewYwBvlWsDmw==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.5.tgz", + "integrity": "sha512-5u0IuAt1r5e2u2vSKhp3phnaf6hH89B/q7GErfPse1sdDfNI6wHVppxai28PAfAj9gwooJun6MjFWhJFLzS44A==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1392,16 +1095,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.0.5", - "@angular/compiler": "18.0.5", - "@angular/core": "18.0.5", - "@angular/platform-browser": "18.0.5" + "@angular/common": "18.2.5", + "@angular/compiler": "18.2.5", + "@angular/core": "18.2.5", + "@angular/platform-browser": "18.2.5" } }, "node_modules/@angular/router": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.0.5.tgz", - "integrity": "sha512-GmdzD5FZYPKCGP6mV3AZraAU6czfGcjjCym6mIsdJr3DyMwnQSwaaHAu8qlQbPDVfsP+gKVSPh1JxI1lzzarLA==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.5.tgz", + "integrity": "sha512-OjZV1PTiSwT0ytmR0ykveLYzs4uQWf0EuIclZmWqM/bb8Q4P+gJl7/sya05nGnZsj6nHGOL0e/LhSZ3N+5p6qg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1410,16 +1113,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.0.5", - "@angular/core": "18.0.5", - "@angular/platform-browser": "18.0.5", + "@angular/common": "18.2.5", + "@angular/core": "18.2.5", + "@angular/platform-browser": "18.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.0.5.tgz", - "integrity": "sha512-Uz3rKHY0pBOvAfxhaGI9X8glS8oaPv03e3GsucZhzuDCijQGHQb1Plaz56NntIGvGaghLMq3zwV7YLPnquarvw==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.5.tgz", + "integrity": "sha512-MoF2n7z/X+yqK89mIRHQutVHIBTyEUo/fDEL8LcuBP4KOZmX9cRoCEt+vqH49BkArsgOM0jNFMYCM8yt0jg7pw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1431,8 +1134,8 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.0.5", - "@angular/core": "18.0.5" + "@angular/common": "18.2.5", + "@angular/core": "18.2.5" } }, "node_modules/@babel/code-frame": { @@ -3315,10 +3018,9 @@ } }, "node_modules/@capacitor-community/file-opener": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@capacitor-community/file-opener/-/file-opener-6.0.0.tgz", - "integrity": "sha512-nJ9S5rCqnVDBKfqdjDhrYOIO9JLeScFkRfKLs2G+d6Df73vrJMes8dr+dGSEvKiPhyjRhICW5imDJEbzaD8KpA==", - "license": "MIT", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@capacitor-community/file-opener/-/file-opener-6.0.1.tgz", + "integrity": "sha512-6DMcCVZPWnx1ewlCcciDGQ9n+hZt7ixLuSMv5U2epyZJ44vdLXEKjZvwU+Wpdpmsq5p0pT9jExDODFdc+DNCsw==", "engines": { "node": ">=16.0.0", "npm": ">=8.0.0" @@ -3679,15 +3381,15 @@ } }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz", - "integrity": "sha512-C3Axuq1xd/9VqFZpW4YAzOx5O9q/LP46uIQy/iNDpHG3fmPa6TBtvfglMCs3RBiBxAIi0Go97r8+jvTt55XMyQ==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.48.0.tgz", + "integrity": "sha512-G6QUWIcC+KvSwXNsJyDTHvqUdNoAVJPPgkc3+Uk4WBKqZvoXhlvazOgm9aL0HwihJLQf0l+tOE2UFzXBqCqgDw==", "dev": true, "license": "MIT", "dependencies": { "comment-parser": "1.4.1", "esquery": "^1.6.0", - "jsdoc-type-pratt-parser": "~4.0.0" + "jsdoc-type-pratt-parser": "~4.1.0" }, "engines": { "node": ">=16" @@ -4355,15 +4057,15 @@ } }, "node_modules/@inquirer/checkbox": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.4.7.tgz", - "integrity": "sha512-5YwCySyV1UEgqzz34gNsC38eKxRBtlRDpJLlKcRtTjlYA/yDKuc1rfw+hjw+2WJxbAZtaDPsRl5Zk7J14SBoBw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.5.0.tgz", + "integrity": "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", + "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", + "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -4386,19 +4088,18 @@ } }, "node_modules/@inquirer/core": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.0.10.tgz", - "integrity": "sha512-TdESOKSVwf6+YWDz8GhS6nKscwzkIyakEzCLJ5Vh6O3Co2ClhCJ0A4MG909MUWfaWdpJm7DE45ii51/2Kat9tA==", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", "@types/mute-stream": "^0.0.4", - "@types/node": "^22.1.0", + "@types/node": "^22.5.5", "@types/wrap-ansi": "^3.0.0", "ansi-escapes": "^4.3.2", - "cli-spinners": "^2.9.2", "cli-width": "^4.1.0", "mute-stream": "^1.0.0", "signal-exit": "^4.1.0", @@ -4410,10 +4111,23 @@ "node": ">=18" } }, + "node_modules/@inquirer/core/node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@inquirer/core/node_modules/@types/node": { - "version": "22.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", - "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "version": "22.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.6.1.tgz", + "integrity": "sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw==", "dev": true, "license": "MIT", "dependencies": { @@ -4421,14 +4135,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.1.22.tgz", - "integrity": "sha512-K1QwTu7GCK+nKOVRBp5HY9jt3DXOfPGPr6WRDrPImkcJRelG9UTx2cAtK1liXmibRrzJlTWOwqgWT3k2XnS62w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.2.0.tgz", + "integrity": "sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", "external-editor": "^3.1.0" }, "engines": { @@ -4436,14 +4150,14 @@ } }, "node_modules/@inquirer/expand": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.1.22.tgz", - "integrity": "sha512-wTZOBkzH+ItPuZ3ZPa9lynBsdMp6kQ9zbjVPYEtSBG7UulGjg2kQiAnUjgyG4SlntpTce5bOmXAPvE4sguXjpA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.3.0.tgz", + "integrity": "sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -4451,9 +4165,9 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.5.tgz", - "integrity": "sha512-79hP/VWdZ2UVc9bFGJnoQ/lQMpL74mGgzSYX1xUqCVk7/v73vJCMw1VuyWN1jGkZ9B3z7THAbySqGbCNefcjfA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.6.tgz", + "integrity": "sha512-yfZzps3Cso2UbM7WlxKwZQh2Hs6plrbjs1QnzQDZhK2DgyCo6D8AaHps9olkNcUFlcYERMqU3uJSp1gmy3s/qQ==", "dev": true, "license": "MIT", "engines": { @@ -4461,28 +4175,42 @@ } }, "node_modules/@inquirer/input": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.2.9.tgz", - "integrity": "sha512-7Z6N+uzkWM7+xsE+3rJdhdG/+mQgejOVqspoW+w0AbSZnL6nq5tGMEVASaYVWbkoSzecABWwmludO2evU3d31g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.3.0.tgz", + "integrity": "sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2" + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.1.0.tgz", + "integrity": "sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" }, "engines": { "node": ">=18" } }, "node_modules/@inquirer/password": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.1.22.tgz", - "integrity": "sha512-5Fxt1L9vh3rAKqjYwqsjU4DZsEvY/2Gll+QkqR4yEpy6wvzLxdSgFhUcxfDAOtO4BEoTreWoznC0phagwLU5Kw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.2.0.tgz", + "integrity": "sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2" }, "engines": { @@ -4490,34 +4218,52 @@ } }, "node_modules/@inquirer/prompts": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.0.7.tgz", - "integrity": "sha512-GFcigCxJTKCH3aECzMIu4FhgLJWnFvMXzpI4CCSoELWFtkOOU2P+goYA61+OKpGrB8fPE7q6n8zAXBSlZRrHjQ==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.3.8.tgz", + "integrity": "sha512-b2BudQY/Si4Y2a0PdZZL6BeJtl8llgeZa7U2j47aaJSCeAl1e4UI7y8a9bSkO3o/ZbZrgT5muy/34JbsjfIWxA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^2.3.7", - "@inquirer/confirm": "^3.1.11", - "@inquirer/editor": "^2.1.11", - "@inquirer/expand": "^2.1.11", - "@inquirer/input": "^2.1.11", - "@inquirer/password": "^2.1.11", - "@inquirer/rawlist": "^2.1.11", - "@inquirer/select": "^2.3.7" + "@inquirer/checkbox": "^2.4.7", + "@inquirer/confirm": "^3.1.22", + "@inquirer/editor": "^2.1.22", + "@inquirer/expand": "^2.1.22", + "@inquirer/input": "^2.2.9", + "@inquirer/number": "^1.0.10", + "@inquirer/password": "^2.1.22", + "@inquirer/rawlist": "^2.2.4", + "@inquirer/search": "^1.0.7", + "@inquirer/select": "^2.4.7" }, "engines": { "node": ">=18" } }, "node_modules/@inquirer/rawlist": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.2.4.tgz", - "integrity": "sha512-pb6w9pWrm7EfnYDgQObOurh2d2YH07+eDo3xQBsNAM2GRhliz6wFXGi1thKQ4bN6B0xDd6C3tBsjdr3obsCl3Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.3.0.tgz", + "integrity": "sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.1.0.tgz", + "integrity": "sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -4525,15 +4271,15 @@ } }, "node_modules/@inquirer/select": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.4.7.tgz", - "integrity": "sha512-JH7XqPEkBpNWp3gPCqWqY8ECbyMoFcCZANlL6pV9hf59qK6dGmkOlx1ydyhY+KZ0c5X74+W6Mtp+nm2QX0/MAQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.5.0.tgz", + "integrity": "sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", + "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", + "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -4542,9 +4288,9 @@ } }, "node_modules/@inquirer/type": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.2.tgz", - "integrity": "sha512-w9qFkumYDCNyDZmNQjf/n6qQuvQ4dMC3BJesY4oF+yr0CxR5vxujflAVeIcS6U336uzi9GM0kAfZlLrZ9UTkpA==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", "dev": true, "license": "MIT", "dependencies": { @@ -5754,13 +5500,13 @@ "license": "MIT" }, "node_modules/@listr2/prompt-adapter-inquirer": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.13.tgz", - "integrity": "sha512-nAl6teTt7EWSjttNavAnv3uFR3w3vPP3OTYmHyPNHzKhAj2NoBDHmbS3MGpvvO8KXXPASnHjEGrrKrdKTMKPnQ==", + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.15.tgz", + "integrity": "sha512-MZrGem/Ujjd4cPTLYDfCZK2iKKeiO/8OX13S6jqxldLs0Prf2aGqVlJ77nMBqMv7fzqgXEgjrNHLXcKR8l9lOg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/type": "^1.3.3" + "@inquirer/type": "^1.5.1" }, "engines": { "node": ">=18.0.0" @@ -5938,9 +5684,9 @@ ] }, "node_modules/@ngtools/webpack": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.2.tgz", - "integrity": "sha512-YhADmc+lVjLt3kze07A+yLry2yzcghdclu+7D3EDfa6fG2Pk33HK3MY2I0Z0BO+Ivoq7cV7yxm+naR+Od0Y5ng==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.6.tgz", + "integrity": "sha512-7HwOPE1EOgcHnpt4brSiT8G2CcXB50G0+CbCBaKGy4LYCG3Y3mrlzF5Fup9HvMJ6Tzqd62RqzpKKYBiGUT7hxg==", "dev": true, "license": "MIT", "engines": { @@ -5955,9 +5701,9 @@ } }, "node_modules/@ngx-formly/core": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.3.6.tgz", - "integrity": "sha512-0GDllrb9fFBTKG+yT+iQf96N3/CN+qRXIYsSX3uft12+c28qKVfMTsWTPYQsmKfGcrqtOZkMVTc+jGGD2JLZLg==", + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.3.7.tgz", + "integrity": "sha512-To2mH09YSm3nyThABNHIameIJCPA9C+x3/JFxFtBWek+UbYeW9DYOqNHRCc7P1ToqLqNEuwrmzjB2YSA8pO9Pw==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -5968,22 +5714,22 @@ } }, "node_modules/@ngx-formly/ionic": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/@ngx-formly/ionic/-/ionic-6.3.6.tgz", - "integrity": "sha512-GaZav6bGGuQ3BqEVYK9DV+QsdM92jjfPmKbN9qz5s+kXH4ahjGfMqcq6Rm4SP49vvl5Am3mJZbZU4g9XrJI5tQ==", + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/@ngx-formly/ionic/-/ionic-6.3.7.tgz", + "integrity": "sha512-j3jiv51CVNeJGY02bZgizarO24DF5AdzL5HJ7SAtJqBIxJNR++AkQZZvgMYtPYTrvhdOIiZrhkBWh0x+rfQMJQ==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@ionic/angular": "^6.0.0 || ^7.0.0", - "@ngx-formly/core": "6.3.6" + "@ngx-formly/core": "6.3.7" } }, "node_modules/@ngx-formly/schematics": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/@ngx-formly/schematics/-/schematics-6.3.6.tgz", - "integrity": "sha512-QdrvdL4YrfhU9AxIXczSyzbZHWq7uuDtsIeEZ3lC0dFyvA0YyTxZRWfNyyMwCXCRXvn70WGlaU8UpeahTXsoAg==", + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/@ngx-formly/schematics/-/schematics-6.3.7.tgz", + "integrity": "sha512-e1Y7RNa6AGK+YEIzNXNX5lvA8MrFQ9UEL/Pj8zwGdotKI8CnNyRVHneQ/1F+QZBo1mLXUtXJnejPoOMkGfo7VQ==", "license": "MIT", "dependencies": { "@angular-devkit/core": "^13.0.3", @@ -6492,9 +6238,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz", - "integrity": "sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.23.0.tgz", + "integrity": "sha512-8OR+Ok3SGEMsAZispLx8jruuXw0HVF16k+ub2eNXKHDmdxL4cf9NlNpAzhlOhNyXzKDEJuFeq0nZm+XlNb1IFw==", "cpu": [ "arm" ], @@ -6506,9 +6252,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz", - "integrity": "sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.23.0.tgz", + "integrity": "sha512-rEFtX1nP8gqmLmPZsXRMoLVNB5JBwOzIAk/XAcEPuKrPa2nPJ+DuGGpfQUR0XjRm8KjHfTZLpWbKXkA5BoFL3w==", "cpu": [ "arm64" ], @@ -6520,9 +6266,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz", - "integrity": "sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.23.0.tgz", + "integrity": "sha512-ZbqlMkJRMMPeapfaU4drYHns7Q5MIxjM/QeOO62qQZGPh9XWziap+NF9fsqPHT0KzEL6HaPspC7sOwpgyA3J9g==", "cpu": [ "arm64" ], @@ -6534,9 +6280,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz", - "integrity": "sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.23.0.tgz", + "integrity": "sha512-PfmgQp78xx5rBCgn2oYPQ1rQTtOaQCna0kRaBlc5w7RlA3TDGGo7m3XaptgitUZ54US9915i7KeVPHoy3/W8tA==", "cpu": [ "x64" ], @@ -6548,9 +6294,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz", - "integrity": "sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.23.0.tgz", + "integrity": "sha512-WAeZfAAPus56eQgBioezXRRzArAjWJGjNo/M+BHZygUcs9EePIuGI1Wfc6U/Ki+tMW17FFGvhCfYnfcKPh18SA==", "cpu": [ "arm" ], @@ -6562,9 +6308,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz", - "integrity": "sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.23.0.tgz", + "integrity": "sha512-v7PGcp1O5XKZxKX8phTXtmJDVpE20Ub1eF6w9iMmI3qrrPak6yR9/5eeq7ziLMrMTjppkkskXyxnmm00HdtXjA==", "cpu": [ "arm" ], @@ -6576,9 +6322,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz", - "integrity": "sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.23.0.tgz", + "integrity": "sha512-nAbWsDZ9UkU6xQiXEyXBNHAKbzSAi95H3gTStJq9UGiS1v+YVXwRHcQOQEF/3CHuhX5BVhShKoeOf6Q/1M+Zhg==", "cpu": [ "arm64" ], @@ -6590,9 +6336,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz", - "integrity": "sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.23.0.tgz", + "integrity": "sha512-5QT/Di5FbGNPaVw8hHO1wETunwkPuZBIu6W+5GNArlKHD9fkMHy7vS8zGHJk38oObXfWdsuLMogD4sBySLJ54g==", "cpu": [ "arm64" ], @@ -6604,9 +6350,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz", - "integrity": "sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.23.0.tgz", + "integrity": "sha512-Sefl6vPyn5axzCsO13r1sHLcmPuiSOrKIImnq34CBurntcJ+lkQgAaTt/9JkgGmaZJ+OkaHmAJl4Bfd0DmdtOQ==", "cpu": [ "ppc64" ], @@ -6618,9 +6364,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz", - "integrity": "sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.23.0.tgz", + "integrity": "sha512-o4QI2KU/QbP7ZExMse6ULotdV3oJUYMrdx3rBZCgUF3ur3gJPfe8Fuasn6tia16c5kZBBw0aTmaUygad6VB/hQ==", "cpu": [ "riscv64" ], @@ -6632,9 +6378,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz", - "integrity": "sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.23.0.tgz", + "integrity": "sha512-+bxqx+V/D4FGrpXzPGKp/SEZIZ8cIW3K7wOtcJAoCrmXvzRtmdUhYNbgd+RztLzfDEfA2WtKj5F4tcbNPuqgeg==", "cpu": [ "s390x" ], @@ -6646,9 +6392,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz", - "integrity": "sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.23.0.tgz", + "integrity": "sha512-I/eXsdVoCKtSgK9OwyQKPAfricWKUMNCwJKtatRYMmDo5N859tbO3UsBw5kT3dU1n6ZcM1JDzPRSGhAUkxfLxw==", "cpu": [ "x64" ], @@ -6660,9 +6406,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz", - "integrity": "sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.23.0.tgz", + "integrity": "sha512-4ZoDZy5ShLbbe1KPSafbFh1vbl0asTVfkABC7eWqIs01+66ncM82YJxV2VtV3YVJTqq2P8HMx3DCoRSWB/N3rw==", "cpu": [ "x64" ], @@ -6674,9 +6420,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz", - "integrity": "sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.23.0.tgz", + "integrity": "sha512-+5Ky8dhft4STaOEbZu3/NU4QIyYssKO+r1cD3FzuusA0vO5gso15on7qGzKdNXnc1gOrsgCqZjRw1w+zL4y4hQ==", "cpu": [ "arm64" ], @@ -6688,9 +6434,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz", - "integrity": "sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.23.0.tgz", + "integrity": "sha512-0SPJk4cPZQhq9qA1UhIRumSE3+JJIBBjtlGl5PNC///BoaByckNZd53rOYD0glpTkYFBQSt7AkMeLVPfx65+BQ==", "cpu": [ "ia32" ], @@ -6702,9 +6448,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz", - "integrity": "sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.23.0.tgz", + "integrity": "sha512-lqCK5GQC8fNo0+JvTSxcG7YB1UKYp8yrNLhsArlvPWN+16ovSZgoehlVHg6X0sSWPUkpjRBR5TuR12ZugowZ4g==", "cpu": [ "x64" ], @@ -6715,63 +6461,23 @@ "win32" ] }, - "node_modules/@schematics/angular": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.1.0.tgz", - "integrity": "sha512-k9Dy6JD7hqvCzDqnMjDm7J8H/P6m5mLuX2yEgQWKRAJ/YMINtBQAaKA1T9qXk97kEX6RNLpHMuDIsrIfK/H31Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "18.1.0", - "@angular-devkit/schematics": "18.1.0", - "jsonc-parser": "3.3.1" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.1.0.tgz", - "integrity": "sha512-6eXQDzHZCbpSMLv9Ohl+1QyLVDmGEXpuuHz3y64LfUTP0aEiBaxk96FjLXIxzJ4f2pbbW2XHzc+yuboGToRA0w==", + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.16.0", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.2", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } + "license": "MIT" }, - "node_modules/@schematics/angular/node_modules/@angular-devkit/schematics": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.1.0.tgz", - "integrity": "sha512-BjrYutLfYFiPOSEcLBWCj3ENkwDn8gMfBSJesaBz7OrZBZGK5j0dVgBLIsGTP96TKo4o4vszJQOvS4AtV6xMGg==", + "node_modules/@schematics/angular": { + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.5.tgz", + "integrity": "sha512-tBXhk9OGT4U6VsBNbuCNl2ITDOF3NYdGrEieIHU+lHSkpJNGZUIGxCgXCETXkmXDq1pe4wFZSKelWjeqYDfX0g==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.1.0", - "jsonc-parser": "3.3.1", - "magic-string": "0.30.10", - "ora": "5.4.1", - "rxjs": "7.8.1" + "@angular-devkit/core": "18.2.5", + "@angular-devkit/schematics": "18.2.5", + "jsonc-parser": "3.3.1" }, "engines": { "node": "^18.19.1 || ^20.11.1 || >=22.0.0", @@ -6779,43 +6485,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@schematics/angular/node_modules/ajv": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", - "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@schematics/angular/node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - }, - "node_modules/@schematics/angular/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/@sigstore/bundle": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.2.tgz", @@ -6930,14 +6599,12 @@ } }, "node_modules/@stylistic/eslint-plugin": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.7.2.tgz", - "integrity": "sha512-3DVLU5HEuk2pQoBmXJlzvrxbKNpu2mJ0SRqz5O/CJjyNCr12ZiPcYMEtuArTyPOk5i7bsAU44nywh1rGfe3gKQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.8.0.tgz", + "integrity": "sha512-Ufvk7hP+bf+pD35R/QfunF793XlSRIC7USr3/EdgduK9j13i2JjmsM0LUz3/foS+jDYp2fzyWZA9N44CPur0Ow==", "dev": true, - "license": "MIT", "dependencies": { - "@types/eslint": "^9.6.1", - "@typescript-eslint/utils": "^8.3.0", + "@typescript-eslint/utils": "^8.4.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.1.0", "estraverse": "^5.3.0", @@ -7150,21 +6817,10 @@ "@types/node": "*" } }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "license": "MIT" }, @@ -7182,9 +6838,22 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.5", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", - "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", + "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "dev": true, "license": "MIT", "dependencies": { @@ -7311,9 +6980,9 @@ "license": "MIT" }, "node_modules/@types/qs": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", "dev": true, "license": "MIT" }, @@ -7424,9 +7093,218 @@ "@typescript-eslint/type-utils": "7.18.0", "@typescript-eslint/utils": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { @@ -7436,27 +7314,21 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -7464,52 +7336,33 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", - "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "node_modules/@typescript-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, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4" - }, + "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "url": "https://opencollective.com/eslint" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz", + "integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -7544,17 +7397,15 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" + "@typescript-eslint/visitor-keys": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -7562,12 +7413,9 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" } }, - "node_modules/@typescript-eslint/types": { + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", @@ -7581,7 +7429,7 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/typescript-estree": { + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", @@ -7610,53 +7458,65 @@ } } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", - "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/typescript-estree": "8.3.0" + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.56.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", - "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0" + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", - "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", + "node_modules/@typescript-eslint/type-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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz", + "integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -7665,15 +7525,15 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", - "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz", + "integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -7694,15 +7554,16 @@ } } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", - "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", + "node_modules/@typescript-eslint/utils": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz", + "integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.3.0", - "eslint-visitor-keys": "^3.4.3" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.7.0", + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/typescript-estree": "8.7.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7710,33 +7571,23 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-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, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz", + "integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/types": "8.7.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -8122,16 +7973,15 @@ } }, "node_modules/ajv": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", - "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -8856,11 +8706,10 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, - "license": "MIT", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -8870,7 +8719,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -9474,9 +9323,9 @@ } }, "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -9500,9 +9349,9 @@ } }, "node_modules/cli-truncate/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true, "license": "MIT" }, @@ -12303,9 +12152,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.2.tgz", - "integrity": "sha512-3XnC5fDyc8M4J2E8pt8pmSVRX2M+5yWMCfI/kDZwauQeFgzQOuhcRBFKjTeJagqgk4sFKxe1mvNVnaWwImx/Tg==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.11.1.tgz", + "integrity": "sha512-EwcbfLOhwVMAfatfqLecR2yv3dE5+kQ8kx+Rrt0DvDXEVwW86KQ/xbMDQhtp5l42VXukD5SOF8mQQHbaNtO0CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12331,27 +12180,28 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz", + "integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==", "dev": true, "license": "MIT", "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.9.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", "tsconfig-paths": "^3.15.0" }, @@ -12420,17 +12270,18 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "48.10.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-48.10.0.tgz", - "integrity": "sha512-BEli0k8E0dzhJairAllwlkGnyYDZVKNn4WDmyKy+v6J5qGNuofjzxwNUi+55BOGmyO9mKBhqaidwGy+dxndn/Q==", + "version": "50.2.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.2.4.tgz", + "integrity": "sha512-020jA+dXaXdb+TML3ZJBvpPmzwbNROjnYuTYi/g6A5QEmEjhptz4oPJDKkOGMIByNxsPpdTLzSU1HYVqebOX1w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@es-joy/jsdoccomment": "~0.46.0", + "@es-joy/jsdoccomment": "~0.48.0", "are-docs-informative": "^0.0.2", "comment-parser": "1.4.1", - "debug": "^4.3.5", + "debug": "^4.3.6", "escape-string-regexp": "^4.0.0", + "espree": "^10.1.0", "esquery": "^1.6.0", "parse-imports": "^2.1.1", "semver": "^7.6.3", @@ -12468,11 +12319,10 @@ } }, "node_modules/eslint-plugin-unused-imports": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.3.tgz", - "integrity": "sha512-lqrNZIZjFMUr7P06eoKtQLwyVRibvG7N+LtfKtObYGizAAGrcqLkc3tDx+iAik2z7q0j/XI3ihjupIqxhFabFA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz", + "integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==", "dev": true, - "license": "MIT", "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "eslint": "^9.0.0 || ^8.0.0" @@ -12903,38 +12753,38 @@ "license": "Apache-2.0" }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -12965,15 +12815,25 @@ "ms": "2.0.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/express/node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -13113,8 +12973,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/fastq": { "version": "1.17.1", @@ -15613,9 +15472,9 @@ } }, "node_modules/jasmine-core": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.5.0.tgz", - "integrity": "sha512-9PMzyvhtocxb3aXJVOPqBDswdgyAeSB81QnLop4npOpbqnheaTEwPc9ZloQeVswugPManznQBjD8kWDTjlnHuw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.3.0.tgz", + "integrity": "sha512-zsOmeBKESky4toybvWEikRiZ0jHoBEu79wNArLfMdSnlLMZx3Xcp6CSm2sUcYyoJC+Uyj8LBJap/MUbVSfJ27g==", "dev": true, "license": "MIT" }, @@ -15725,9 +15584,9 @@ "license": "MIT" }, "node_modules/jsdoc-type-pratt-parser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", - "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", + "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", "dev": true, "license": "MIT", "engines": { @@ -16211,6 +16070,13 @@ "karma-jasmine": "^5.0.0" } }, + "node_modules/karma-jasmine/node_modules/jasmine-core": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", + "dev": true, + "license": "MIT" + }, "node_modules/karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -16388,9 +16254,9 @@ } }, "node_modules/launch-editor": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.8.1.tgz", - "integrity": "sha512-elBx2l/tp9z99X5H/qev8uyDywVh0VXAwEbjk8kJhnc5grOFkGh7aW6q55me9xnYbss261XtnUrysZ+XvGbhQA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", "dev": true, "license": "MIT", "dependencies": { @@ -16593,16 +16459,16 @@ "license": "MIT" }, "node_modules/listr2": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.3.tgz", - "integrity": "sha512-Lllokma2mtoniUOS94CcOErHWAug5iu7HOmDrvWgpw8jyQH2fomgB+7lZS4HWZxytUuQwkGOwe49FvwVaA85Xw==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", + "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", "dev": true, "license": "MIT", "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", - "log-update": "^6.0.0", + "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" }, @@ -16611,9 +16477,9 @@ } }, "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -16637,9 +16503,9 @@ } }, "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true, "license": "MIT" }, @@ -17064,9 +16930,9 @@ } }, "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -17106,9 +16972,9 @@ } }, "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true, "license": "MIT" }, @@ -17275,7 +17141,6 @@ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, - "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } @@ -17351,9 +17216,9 @@ } }, "node_modules/memfs": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.11.1.tgz", - "integrity": "sha512-LZcMTBAgqUUKNXZagcZxvXXfgF1bHX7Y7nQ0QyEiNbRJgE29GhgPd8Yna1VQcLlPiHt/5RFJMWYN9Uv/VPNvjQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.12.0.tgz", + "integrity": "sha512-74wDsex5tQDSClVkeK1vtxqYCAgCoXxx+K4NSHzgU/muYVYByFqa+0RnrPO9NM6naWm1+G9JmZ0p6QHhXmeYfA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -17550,11 +17415,14 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -18670,9 +18538,9 @@ } }, "node_modules/npm-package-arg": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.2.tgz", - "integrity": "sha512-IGN0IAwmhDJwy13Wc8k+4PEbTPhpJnMtfR53ZbOyjkvmEcLS4nCwp6mvMWjS5sUjeiW3mpx6cHmuhKEu9XmcQw==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", "dev": true, "license": "ISC", "dependencies": { @@ -18719,9 +18587,9 @@ } }, "node_modules/npm-pick-manifest": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.1.tgz", - "integrity": "sha512-Udm1f0l2nXb3wxDpKjfohwgdFUSV50UVwzEIpDXVsbDMXVIEF81a/i0UhuQbhrPMMmdiq3+YMFLFIRVLs3hxQw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.1.0.tgz", + "integrity": "sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==", "dev": true, "license": "ISC", "dependencies": { @@ -19094,9 +18962,9 @@ } }, "node_modules/ordered-binary": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.1.tgz", - "integrity": "sha512-5VyHfHY3cd0iza71JepYG50My+YUbrFtGoUz2ooEydPyPM7Aai/JW098juLr+RG6+rDJuzNNTsEQu2DZa1A41A==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.2.tgz", + "integrity": "sha512-JTo+4+4Fw7FreyAvlSLjb1BBVaxEQAacmjD3jjuyPZclpbEghTvQZbXBb2qPd2LeIMxiHwXBZUcpmG2Gl/mDEA==", "dev": true, "license": "MIT" }, @@ -19476,9 +19344,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "dev": true, "license": "MIT" }, @@ -19507,11 +19375,10 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true, - "license": "ISC" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true }, "node_modules/picomatch": { "version": "4.0.2", @@ -20395,13 +20262,12 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -21306,13 +21172,13 @@ "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz", - "integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.23.0.tgz", + "integrity": "sha512-vXB4IT9/KLDrS2WRXmY22sVB2wTsTwkpxjB8Q3mnakTENcYw3FRmfdYDy/acNmls+lHmDazgrRjK/yQ6hQAtwA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -21322,22 +21188,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.2", - "@rollup/rollup-android-arm64": "4.21.2", - "@rollup/rollup-darwin-arm64": "4.21.2", - "@rollup/rollup-darwin-x64": "4.21.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.2", - "@rollup/rollup-linux-arm-musleabihf": "4.21.2", - "@rollup/rollup-linux-arm64-gnu": "4.21.2", - "@rollup/rollup-linux-arm64-musl": "4.21.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.2", - "@rollup/rollup-linux-riscv64-gnu": "4.21.2", - "@rollup/rollup-linux-s390x-gnu": "4.21.2", - "@rollup/rollup-linux-x64-gnu": "4.21.2", - "@rollup/rollup-linux-x64-musl": "4.21.2", - "@rollup/rollup-win32-arm64-msvc": "4.21.2", - "@rollup/rollup-win32-ia32-msvc": "4.21.2", - "@rollup/rollup-win32-x64-msvc": "4.21.2", + "@rollup/rollup-android-arm-eabi": "4.23.0", + "@rollup/rollup-android-arm64": "4.23.0", + "@rollup/rollup-darwin-arm64": "4.23.0", + "@rollup/rollup-darwin-x64": "4.23.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.23.0", + "@rollup/rollup-linux-arm-musleabihf": "4.23.0", + "@rollup/rollup-linux-arm64-gnu": "4.23.0", + "@rollup/rollup-linux-arm64-musl": "4.23.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.23.0", + "@rollup/rollup-linux-riscv64-gnu": "4.23.0", + "@rollup/rollup-linux-s390x-gnu": "4.23.0", + "@rollup/rollup-linux-x64-gnu": "4.23.0", + "@rollup/rollup-linux-x64-musl": "4.23.0", + "@rollup/rollup-win32-arm64-msvc": "4.23.0", + "@rollup/rollup-win32-ia32-msvc": "4.23.0", + "@rollup/rollup-win32-x64-msvc": "4.23.0", "fsevents": "~2.3.2" } }, @@ -21740,9 +21606,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "license": "MIT", "dependencies": { @@ -21898,21 +21764,31 @@ "license": "ISC" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -22403,11 +22279,10 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -22989,9 +22864,9 @@ } }, "node_modules/swiper": { - "version": "11.1.11", - "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.1.11.tgz", - "integrity": "sha512-077Aw3OrlZpkkBRf/6+44bGh/HZY/vsLEyate2db2KkJgYUIR5TvDgvvhcJtW/puXzw79w5KBc30DauEX6GZYQ==", + "version": "11.1.14", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.1.14.tgz", + "integrity": "sha512-VbQLQXC04io6AoAjIUWuZwW4MSYozkcP9KjLdrsG/00Q/yiwvhz9RQyt0nHXV10hi9NVnDNy1/wv7Dzq1lkOCQ==", "funding": [ { "type": "patreon", @@ -23002,7 +22877,6 @@ "url": "http://opencollective.com/swiper" } ], - "license": "MIT", "engines": { "node": ">= 4.7.0" } @@ -24236,15 +24110,15 @@ } }, "node_modules/vite": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", - "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.40", - "rollup": "^4.13.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -24725,6 +24599,35 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/vite/node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/void-elements": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", @@ -25045,9 +24948,9 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.3.0.tgz", - "integrity": "sha512-xD2qnNew+F6KwOGZR7kWdbIou/ud7cVqLEXeK1q0nHcNsX/u7ul/fSdlOTX4ntSL5FNFy7ZJJXbf0piF591JYw==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/ui/package.json b/ui/package.json index 0e232477e3c..fdacf2654a7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,30 +1,30 @@ { "name": "openems-ui", - "version": "2024.9.0", + "version": "2024.10.0", "license": "AGPL-3.0", "private": true, "dependencies": { - "@angular/animations": "18.0.5", - "@angular/common": "18.0.5", - "@angular/core": "18.0.5", - "@angular/forms": "18.0.5", - "@angular/platform-browser": "18.0.5", - "@angular/platform-browser-dynamic": "18.0.5", - "@angular/router": "18.0.5", - "@angular/service-worker": "18.0.5", - "@capacitor-community/file-opener": "^6.0.0", - "@capacitor/android": "^6.0.0", - "@capacitor/app": "^6.0.0", - "@capacitor/core": "^6.0.0", - "@capacitor/filesystem": "^6.0.0", - "@capacitor/ios": "^6.0.0", - "@capacitor/splash-screen": "^6.0.0", + "@angular/animations": "18.2.5", + "@angular/common": "18.2.5", + "@angular/core": "18.2.5", + "@angular/forms": "18.2.5", + "@angular/platform-browser": "18.2.5", + "@angular/platform-browser-dynamic": "18.2.5", + "@angular/router": "18.2.5", + "@angular/service-worker": "18.2.5", + "@capacitor-community/file-opener": "^6.0.1", + "@capacitor/android": "^6.1.2", + "@capacitor/app": "^6.0.1", + "@capacitor/core": "^6.1.2", + "@capacitor/filesystem": "^6.0.1", + "@capacitor/ios": "^6.1.2", + "@capacitor/splash-screen": "^6.0.2", "@ionic-native/core": "^5.36.0", "@ionic-native/file-opener": "^5.36.0", "@ionic/angular": "^6.7.5", - "@ngx-formly/core": "^6.3.0", - "@ngx-formly/ionic": "^6.3.6", - "@ngx-formly/schematics": "^6.3.0", + "@ngx-formly/core": "^6.3.7", + "@ngx-formly/ionic": "^6.3.7", + "@ngx-formly/schematics": "^6.3.7", "@ngx-translate/core": "^15.0.0", "@nodro7/angular-mydatepicker": "^0.14.0", "capacitor-blob-writer": "^1.1.17", @@ -46,47 +46,47 @@ "ngx-spinner": "^16.0.2", "roboto-fontface": "^0.10.0", "rxjs": "~6.6.7", - "swiper": "11.1.11", + "swiper": "11.1.14", "tslib": "^2.6.2", "uuid": "^10.0.0", "zone.js": "~0.14.7" }, "devDependencies": { - "@angular-devkit/build-angular": "^18.0.5", - "@angular-devkit/core": "18.0.5", - "@angular-devkit/schematics": "18.0.5", - "@angular-eslint/builder": "^18.1.0", - "@angular-eslint/eslint-plugin": "^18.1.0", - "@angular-eslint/eslint-plugin-template": "^18.1.0", - "@angular-eslint/template-parser": "^18.1.0", - "@angular/cli": "18.1.0", - "@angular/compiler": "18.0.5", - "@angular/compiler-cli": "18.0.5", - "@angular/language-service": "18.0.5", + "@angular-devkit/build-angular": "^18.2.5", + "@angular-devkit/core": "18.2.5", + "@angular-devkit/schematics": "18.2.5", + "@angular-eslint/builder": "^18.3.1", + "@angular-eslint/eslint-plugin": "^18.3.1", + "@angular-eslint/eslint-plugin-template": "^18.3.1", + "@angular-eslint/template-parser": "^18.3.1", + "@angular/cli": "18.2.5", + "@angular/compiler": "18.2.5", + "@angular/compiler-cli": "18.2.5", + "@angular/language-service": "18.2.5", "@capacitor/assets": "^3.0.5", "@capacitor/cli": "6.1.2", "@ionic/angular-toolkit": "^11.0.1", "@ionic/cli": "^7.2.0", - "@stylistic/eslint-plugin": "^2.7.2", + "@stylistic/eslint-plugin": "^2.8.0", "@types/jasmine": "~4.3.6", "@types/jasminewd2": "~2.0.13", "@types/json-schema": "^7.0.15", "@types/node": "^20.12.6", - "@types/qs": "^6.9.15", + "@types/qs": "^6.9.16", "@types/range-parser": "^1.2.7", "@types/send": "^0.17.4", "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", - "@typescript-eslint/types": "^7.0.0", + "@typescript-eslint/types": "^8.7.0", "eslint": "^8.57.0", - "eslint-plugin-import": "2.29.1", - "eslint-plugin-jsdoc": "48.10.0", + "eslint-plugin-import": "2.30.0", + "eslint-plugin-jsdoc": "50.2.4", "eslint-plugin-prefer-arrow": "1.2.3", - "eslint-plugin-unused-imports": "^4.1.3", - "jasmine-core": "~4.5.0", + "eslint-plugin-unused-imports": "^4.1.4", + "jasmine-core": "~5.3.0", "jasmine-spec-reporter": "~7.0.0", - "karma": "~6.4.2", + "karma": "~6.4.4", "karma-chrome-launcher": "~3.2.0", "karma-coverage": "~2.2.1", "karma-coverage-istanbul-reporter": "~3.0.3", diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts index b04097ce80b..f84be2e897f 100644 --- a/ui/src/app/app-routing.module.ts +++ b/ui/src/app/app-routing.module.ts @@ -9,12 +9,14 @@ import { OverviewComponent as ConsumptionChartOverviewComponent } from "./edge/h import { DetailsOverviewComponent as GridDetailsOverviewComponent } from "./edge/history/common/grid/details/details.overview"; import { OverviewComponent as GridChartOverviewComponent } from "./edge/history/common/grid/overview/overview"; import { DetailsOverviewComponent } from "./edge/history/common/production/details/details.overview"; -import { DetailsOverviewComponent as DigitalOutputDetailsOverviewComponent } from "./edge/history/Controller/Io/DigitalOutput/details/details.overview"; import { OverviewComponent as ProductionChartOverviewComponent } from "./edge/history/common/production/overview/overview"; import { OverviewComponent as SelfconsumptionChartOverviewComponent } from "./edge/history/common/selfconsumption/overview/overview"; import { OverviewComponent as ChannelthresholdChartOverviewComponent } from "./edge/history/Controller/ChannelThreshold/overview/overview"; import { OverviewComponent as GridOptimizedChargeChartOverviewComponent } from "./edge/history/Controller/Ess/GridoptimizedCharge/overview/overview"; import { OverviewComponent as TimeOfUseTariffOverviewComponent } from "./edge/history/Controller/Ess/TimeOfUseTariff/overview/overview"; +import { DetailsOverviewComponent as DigitalOutputDetailsOverviewComponent } from "./edge/history/Controller/Io/DigitalOutput/details/details.overview"; +import { OverviewComponent as DigitalOutputChartOverviewComponent } from "./edge/history/Controller/Io/DigitalOutput/overview/overview"; +import { OverviewComponent as ModbusTcpApiOverviewComponent } from "./edge/history/Controller/ModbusTcpApi/overview/overview"; import { DelayedSellToGridChartOverviewComponent } from "./edge/history/delayedselltogrid/symmetricpeakshavingchartoverview/delayedselltogridchartoverview.component"; import { HeatingelementChartOverviewComponent } from "./edge/history/heatingelement/heatingelementchartoverview/heatingelementchartoverview.component"; import { HeatPumpChartOverviewComponent } from "./edge/history/heatpump/heatpumpchartoverview/heatpumpchartoverview.component"; @@ -48,14 +50,12 @@ import { SystemExecuteComponent as EdgeSettingsSystemExecuteComponent } from "./ import { SystemLogComponent as EdgeSettingsSystemLogComponent } from "./edge/settings/systemlog/systemlog.component"; import { LoginComponent } from "./index/login.component"; import { OverViewComponent } from "./index/overview/overview.component"; -import { UserComponent } from "./user/user.component"; import { LoadingScreenComponent } from "./index/shared/loading-screen"; import { CurrentAndVoltageOverviewComponent } from "./shared/components/edge/meter/currentVoltage/currentVoltage.overview"; import { DataService } from "./shared/components/shared/dataservice"; import { hasEdgeRole } from "./shared/guards/functional-guards"; import { Role } from "./shared/type/role"; -import { OverviewComponent as DigitalOutputChartOverviewComponent } from "./edge/history/Controller/Io/DigitalOutput/overview/overview"; - +import { UserComponent } from "./user/user.component"; export const routes: Routes = [ @@ -101,6 +101,7 @@ export const routes: Routes = [ { path: "gridchart", component: GridChartOverviewComponent }, { path: "gridchart/:componentId", component: GridDetailsOverviewComponent }, { path: "gridchart/:componentId/currentVoltage", component: CurrentAndVoltageOverviewComponent }, + { path: ":componentId/modbusTcpApi", component: ModbusTcpApiOverviewComponent }, { path: "productionchart", component: ProductionChartOverviewComponent }, { path: "productionchart/:componentId", component: DetailsOverviewComponent }, { path: "productionchart/:componentId/currentVoltage", component: CurrentAndVoltageOverviewComponent }, diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index bcab88af3be..27d05b4410d 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -8,6 +8,7 @@ import { Subject, Subscription } from "rxjs"; import { filter, takeUntil } from "rxjs/operators"; import { environment } from "../environments"; import { AppService } from "./app.service"; +import { AppStateTracker } from "./shared/ngrx-store/states"; import { GlobalRouteChangeHandler } from "./shared/service/globalRouteChangeHandler"; import { Service, UserPermission, Websocket } from "./shared/shared"; import { Language } from "./shared/type/language"; @@ -42,6 +43,7 @@ export class AppComponent implements OnInit, OnDestroy { private meta: Meta, private appService: AppService, private title: Title, + private stateService: AppStateTracker, ) { service.setLang(Language.getByKey(localStorage.LANGUAGE) ?? Language.getByBrowserLang(navigator.language)); diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 8a191a18baa..e8ad40d998a 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -23,6 +23,7 @@ import { IndexModule } from "./index/index.module"; import { RegistrationModule } from "./registration/registration.module"; import { StatusSingleComponent } from "./shared/components/status/single/status.component"; import { ChartOptionsPopoverComponent } from "./shared/legacy/chartoptions/popover/popover.component"; +import { AppStateTracker } from "./shared/ngrx-store/states"; import { MyErrorHandler } from "./shared/service/myerrorhandler"; import { Pagination } from "./shared/service/pagination"; import { SharedModule } from "./shared/shared.module"; @@ -64,6 +65,7 @@ import { UserModule } from "./user/user.module"; Pagination, CheckForUpdateService, AppService, + AppStateTracker, ], bootstrap: [AppComponent], }) diff --git a/ui/src/app/changelog/view/component/changelog.constants.ts b/ui/src/app/changelog/view/component/changelog.constants.ts index 99de881dd8b..99abdf46dd4 100644 --- a/ui/src/app/changelog/view/component/changelog.constants.ts +++ b/ui/src/app/changelog/view/component/changelog.constants.ts @@ -2,7 +2,7 @@ import { Role } from "src/app/shared/type/role"; export class Changelog { - public static readonly UI_VERSION = "2024.9.0"; + public static readonly UI_VERSION = "2024.10.0"; public static product(...products: Product[]) { return products.map(product => Changelog.link(product.name, product.url)).join(", ") + ". "; diff --git a/ui/src/app/edge/edge.component.ts b/ui/src/app/edge/edge.component.ts index fdeeebc3a3c..c0acc0aa952 100644 --- a/ui/src/app/edge/edge.component.ts +++ b/ui/src/app/edge/edge.component.ts @@ -10,8 +10,6 @@ import { ChannelAddress, Edge, Service, Websocket } from "src/app/shared/shared" template: ` - `, }) export class EdgeComponent implements OnInit, OnDestroy { diff --git a/ui/src/app/edge/history/Controller/ModbusTcpApi/chart/channels.spec.ts b/ui/src/app/edge/history/Controller/ModbusTcpApi/chart/channels.spec.ts new file mode 100644 index 00000000000..d96d89c0015 --- /dev/null +++ b/ui/src/app/edge/history/Controller/ModbusTcpApi/chart/channels.spec.ts @@ -0,0 +1,1070 @@ +import { TimeUnit } from "chart.js"; +import { ChartConstants } from "src/app/shared/components/chart/chart.constants"; +import { OeTester } from "src/app/shared/components/shared/testing/common"; +import { OeChartTester } from "src/app/shared/components/shared/testing/tester"; +import { QueryHistoricTimeseriesDataResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesDataResponse"; +import { QueryHistoricTimeseriesEnergyPerPeriodResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse"; +import { QueryHistoricTimeseriesEnergyResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyResponse"; + +export namespace History { + + export const LINE_CHART_OPTIONS = (period: string, chartType: "line" | "bar", options: { [key: string]: { scale: { min: number, max: number; }, ticks: { stepSize: number; }; }; }): OeChartTester.Dataset.Option => ({ + type: "option", + options: { + "responsive": true, "maintainAspectRatio": false, "elements": { "point": { "radius": 0, "hitRadius": 0, "hoverRadius": 0 }, "line": { "stepped": false, "fill": true } }, "datasets": { "bar": {}, "line": {} }, "plugins": { "colors": { "enabled": false }, "legend": { "display": true, "position": "bottom", "labels": { "color": "" } }, "tooltip": { "intersect": false, "mode": "index", "callbacks": {} } }, "scales": { + "x": { "stacked": true, "offset": false, "type": "time", "ticks": { "source": "auto", "maxTicksLimit": 31 }, "bounds": "ticks", "adapters": { "date": { "locale": { "code": "de", "formatLong": {}, "localize": {}, "match": {}, "options": { "weekStartsOn": 1, "firstWeekContainsDate": 4 } } } }, "time": { "unit": period as TimeUnit, "displayFormats": { "datetime": "yyyy-MM-dd HH:mm:ss", "millisecond": "SSS [ms]", "second": "HH:mm:ss a", "minute": "HH:mm", "hour": "HH:00", "day": "dd", "week": "ll", "month": "MM", "quarter": "[Q]Q - YYYY", "year": "yyyy" } } }, + "left": { + ...options["right"]?.scale, ...(chartType === "line" ? { stacked: false } : {}), "beginAtZero": true, "max": 100, "min": 0, "type": "linear", "title": { "text": "%", "display": true, "font": { "size": 11 }, "padding": 5 }, "position": "right", "grid": { "display": false }, + "ticks": { + ...options["right"]?.ticks, + "color": "", + "padding": 5, + "maxTicksLimit": ChartConstants.NUMBER_OF_Y_AXIS_TICKS, + }, + }, + }, + }, + }); + export const BAR_CHART_OPTIONS = (period: string, chartType: "line" | "bar", options: { [key: string]: { scale: { min?: number, max?: number; }, ticks: { stepSize?: number; }; }; }): OeChartTester.Dataset.Option => ({ + type: "option", + options: { + "responsive": true, "maintainAspectRatio": false, "elements": { "point": { "radius": 0, "hitRadius": 0, "hoverRadius": 0 }, "line": { "stepped": false, "fill": true } }, "datasets": { "bar": { "barPercentage": 1 }, "line": {} }, "plugins": { "colors": { "enabled": false }, "legend": { "display": true, "position": "bottom", "labels": { "color": "" } }, "tooltip": { "intersect": false, "mode": "x", "callbacks": {} } }, "scales": { + "x": { "stacked": true, "offset": true, "type": "time", "ticks": { "source": "auto", "maxTicksLimit": 31 }, "bounds": "ticks", "adapters": { "date": { "locale": { "code": "de", "formatLong": {}, "localize": {}, "match": {}, "options": { "weekStartsOn": 1, "firstWeekContainsDate": 4 } } } }, "time": { "unit": period as TimeUnit, "displayFormats": { "datetime": "yyyy-MM-dd HH:mm:ss", "millisecond": "SSS [ms]", "second": "HH:mm:ss a", "minute": "HH:mm", "hour": "HH:00", "day": "dd", "week": "ll", "month": "MM", "quarter": "[Q]Q - YYYY", "year": "yyyy" } } }, + "left": { + ...options["left"]?.scale, ...(chartType === "line" ? { stacked: false } : {}), "title": { "text": "kWh", "display": true, "padding": 5, "font": { "size": 11 } }, "position": "left", "grid": { "display": true }, + "ticks": { + ...options["left"]?.ticks, + "color": "", + "padding": 5, + "maxTicksLimit": ChartConstants.NUMBER_OF_Y_AXIS_TICKS, + }, + }, + }, + }, + }); + + /** + * up to 288 datapoints (5 min aggregated values) from a + * + * {@link Day.energyPerPeriodChannelWithValues} and {@link Day.dataChannelWithValues} + * */ + export const DAY: OeTester.Types.Channels = ({ + energyChannelWithValues: new QueryHistoricTimeseriesEnergyResponse("0", { + data: {}, + }), + dataChannelWithValues: new QueryHistoricTimeseriesDataResponse("0", { + data: { + "ctrlApiModbusTcp0/Ess0SetActivePowerEquals": [null, + null, + null, + 112, + 262, + 392, + 240, + 230, + 229, + 227, + 317, + 224, + 133, + 135, + 133, + 192, + 209, + 90, + 95, + 96, + 164, + 297, + 184, + 182, + 183, + 198, + 333, + 183, + 93, + 97, + 98, + 197, + 266, + 177, + 144, + 140, + 173, + 304, + 305, + 237, + 232, + 227, + 283, + 344, + 135, + 96, + 95, + null, + null, + null, + null, + 102, + 129, + 140, + 301, + 248, + 267, + 319, + 310, + 452, + 451, + 280, + 234, + 226, + 249, + 390, + 242, + 199, + 179, + 166, + 280, + 239, + 192, + 187, + 187, + 190, + 303, + 146, + 62, + 62, + 64, + 887, + 1119, + 1070, + 1057, + 596, + 138, + 233, + 152, + 209, + 192, + 202, + 308, + 254, + 175, + 122, + 108, + 137, + 216, + 947, + 599, + 203, + 232, + 328, + 299, + 520, + 1213, + 641, + 1030, + 442, + 374, + 1758, + 249, + 260, + 346, + 1879, + 230, + 484, + 1260, + 1317, + 1488, + 1451, + 1892, + 1466, + 1332, + 523, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null], + }, + timestamps: [ + "2023-07-02T22:00:00Z", + "2023-07-02T22:05:00Z", + "2023-07-02T22:10:00Z", + "2023-07-02T22:15:00Z", + "2023-07-02T22:20:00Z", + "2023-07-02T22:25:00Z", + "2023-07-02T22:30:00Z", + "2023-07-02T22:35:00Z", + "2023-07-02T22:40:00Z", + "2023-07-02T22:45:00Z", + "2023-07-02T22:50:00Z", + "2023-07-02T22:55:00Z", + "2023-07-02T23:00:00Z", + "2023-07-02T23:05:00Z", + "2023-07-02T23:10:00Z", + "2023-07-02T23:15:00Z", + "2023-07-02T23:20:00Z", + "2023-07-02T23:25:00Z", + "2023-07-02T23:30:00Z", + "2023-07-02T23:35:00Z", + "2023-07-02T23:40:00Z", + "2023-07-02T23:45:00Z", + "2023-07-02T23:50:00Z", + "2023-07-02T23:55:00Z", + "2023-07-03T00:00:00Z", + "2023-07-03T00:05:00Z", + "2023-07-03T00:10:00Z", + "2023-07-03T00:15:00Z", + "2023-07-03T00:20:00Z", + "2023-07-03T00:25:00Z", + "2023-07-03T00:30:00Z", + "2023-07-03T00:35:00Z", + "2023-07-03T00:40:00Z", + "2023-07-03T00:45:00Z", + "2023-07-03T00:50:00Z", + "2023-07-03T00:55:00Z", + "2023-07-03T01:00:00Z", + "2023-07-03T01:05:00Z", + "2023-07-03T01:10:00Z", + "2023-07-03T01:15:00Z", + "2023-07-03T01:20:00Z", + "2023-07-03T01:25:00Z", + "2023-07-03T01:30:00Z", + "2023-07-03T01:35:00Z", + "2023-07-03T01:40:00Z", + "2023-07-03T01:45:00Z", + "2023-07-03T01:50:00Z", + "2023-07-03T01:55:00Z", + "2023-07-03T02:00:00Z", + "2023-07-03T02:05:00Z", + "2023-07-03T02:10:00Z", + "2023-07-03T02:15:00Z", + "2023-07-03T02:20:00Z", + "2023-07-03T02:25:00Z", + "2023-07-03T02:30:00Z", + "2023-07-03T02:35:00Z", + "2023-07-03T02:40:00Z", + "2023-07-03T02:45:00Z", + "2023-07-03T02:50:00Z", + "2023-07-03T02:55:00Z", + "2023-07-03T03:00:00Z", + "2023-07-03T03:05:00Z", + "2023-07-03T03:10:00Z", + "2023-07-03T03:15:00Z", + "2023-07-03T03:20:00Z", + "2023-07-03T03:25:00Z", + "2023-07-03T03:30:00Z", + "2023-07-03T03:35:00Z", + "2023-07-03T03:40:00Z", + "2023-07-03T03:45:00Z", + "2023-07-03T03:50:00Z", + "2023-07-03T03:55:00Z", + "2023-07-03T04:00:00Z", + "2023-07-03T04:05:00Z", + "2023-07-03T04:10:00Z", + "2023-07-03T04:15:00Z", + "2023-07-03T04:20:00Z", + "2023-07-03T04:25:00Z", + "2023-07-03T04:30:00Z", + "2023-07-03T04:35:00Z", + "2023-07-03T04:40:00Z", + "2023-07-03T04:45:00Z", + "2023-07-03T04:50:00Z", + "2023-07-03T04:55:00Z", + "2023-07-03T05:00:00Z", + "2023-07-03T05:05:00Z", + "2023-07-03T05:10:00Z", + "2023-07-03T05:15:00Z", + "2023-07-03T05:20:00Z", + "2023-07-03T05:25:00Z", + "2023-07-03T05:30:00Z", + "2023-07-03T05:35:00Z", + "2023-07-03T05:40:00Z", + "2023-07-03T05:45:00Z", + "2023-07-03T05:50:00Z", + "2023-07-03T05:55:00Z", + "2023-07-03T06:00:00Z", + "2023-07-03T06:05:00Z", + "2023-07-03T06:10:00Z", + "2023-07-03T06:15:00Z", + "2023-07-03T06:20:00Z", + "2023-07-03T06:25:00Z", + "2023-07-03T06:30:00Z", + "2023-07-03T06:35:00Z", + "2023-07-03T06:40:00Z", + "2023-07-03T06:45:00Z", + "2023-07-03T06:50:00Z", + "2023-07-03T06:55:00Z", + "2023-07-03T07:00:00Z", + "2023-07-03T07:05:00Z", + "2023-07-03T07:10:00Z", + "2023-07-03T07:15:00Z", + "2023-07-03T07:20:00Z", + "2023-07-03T07:25:00Z", + "2023-07-03T07:30:00Z", + "2023-07-03T07:35:00Z", + "2023-07-03T07:40:00Z", + "2023-07-03T07:45:00Z", + "2023-07-03T07:50:00Z", + "2023-07-03T07:55:00Z", + "2023-07-03T08:00:00Z", + "2023-07-03T08:05:00Z", + "2023-07-03T08:10:00Z", + "2023-07-03T08:15:00Z", + "2023-07-03T08:20:00Z", + "2023-07-03T08:25:00Z", + "2023-07-03T08:30:00Z", + "2023-07-03T08:35:00Z", + "2023-07-03T08:40:00Z", + "2023-07-03T08:45:00Z", + "2023-07-03T08:50:00Z", + "2023-07-03T08:55:00Z", + "2023-07-03T09:00:00Z", + "2023-07-03T09:05:00Z", + "2023-07-03T09:10:00Z", + "2023-07-03T09:15:00Z", + "2023-07-03T09:20:00Z", + "2023-07-03T09:25:00Z", + "2023-07-03T09:30:00Z", + "2023-07-03T09:35:00Z", + "2023-07-03T09:40:00Z", + "2023-07-03T09:45:00Z", + "2023-07-03T09:50:00Z", + "2023-07-03T09:55:00Z", + "2023-07-03T10:00:00Z", + "2023-07-03T10:05:00Z", + "2023-07-03T10:10:00Z", + "2023-07-03T10:15:00Z", + "2023-07-03T10:20:00Z", + "2023-07-03T10:25:00Z", + "2023-07-03T10:30:00Z", + "2023-07-03T10:35:00Z", + "2023-07-03T10:40:00Z", + "2023-07-03T10:45:00Z", + "2023-07-03T10:50:00Z", + "2023-07-03T10:55:00Z", + "2023-07-03T11:00:00Z", + "2023-07-03T11:05:00Z", + "2023-07-03T11:10:00Z", + "2023-07-03T11:15:00Z", + "2023-07-03T11:20:00Z", + "2023-07-03T11:25:00Z", + "2023-07-03T11:30:00Z", + "2023-07-03T11:35:00Z", + "2023-07-03T11:40:00Z", + "2023-07-03T11:45:00Z", + "2023-07-03T11:50:00Z", + "2023-07-03T11:55:00Z", + "2023-07-03T12:00:00Z", + "2023-07-03T12:05:00Z", + "2023-07-03T12:10:00Z", + "2023-07-03T12:15:00Z", + "2023-07-03T12:20:00Z", + "2023-07-03T12:25:00Z", + "2023-07-03T12:30:00Z", + "2023-07-03T12:35:00Z", + "2023-07-03T12:40:00Z", + "2023-07-03T12:45:00Z", + "2023-07-03T12:50:00Z", + "2023-07-03T12:55:00Z", + "2023-07-03T13:00:00Z", + "2023-07-03T13:05:00Z", + "2023-07-03T13:10:00Z", + "2023-07-03T13:15:00Z", + "2023-07-03T13:20:00Z", + "2023-07-03T13:25:00Z", + "2023-07-03T13:30:00Z", + "2023-07-03T13:35:00Z", + "2023-07-03T13:40:00Z", + "2023-07-03T13:45:00Z", + "2023-07-03T13:50:00Z", + "2023-07-03T13:55:00Z", + "2023-07-03T14:00:00Z", + "2023-07-03T14:05:00Z", + "2023-07-03T14:10:00Z", + "2023-07-03T14:15:00Z", + "2023-07-03T14:20:00Z", + "2023-07-03T14:25:00Z", + "2023-07-03T14:30:00Z", + "2023-07-03T14:35:00Z", + "2023-07-03T14:40:00Z", + "2023-07-03T14:45:00Z", + "2023-07-03T14:50:00Z", + "2023-07-03T14:55:00Z", + "2023-07-03T15:00:00Z", + "2023-07-03T15:05:00Z", + "2023-07-03T15:10:00Z", + "2023-07-03T15:15:00Z", + "2023-07-03T15:20:00Z", + "2023-07-03T15:25:00Z", + "2023-07-03T15:30:00Z", + "2023-07-03T15:35:00Z", + "2023-07-03T15:40:00Z", + "2023-07-03T15:45:00Z", + "2023-07-03T15:50:00Z", + "2023-07-03T15:55:00Z", + "2023-07-03T16:00:00Z", + "2023-07-03T16:05:00Z", + "2023-07-03T16:10:00Z", + "2023-07-03T16:15:00Z", + "2023-07-03T16:20:00Z", + "2023-07-03T16:25:00Z", + "2023-07-03T16:30:00Z", + "2023-07-03T16:35:00Z", + "2023-07-03T16:40:00Z", + "2023-07-03T16:45:00Z", + "2023-07-03T16:50:00Z", + "2023-07-03T16:55:00Z", + "2023-07-03T17:00:00Z", + "2023-07-03T17:05:00Z", + "2023-07-03T17:10:00Z", + "2023-07-03T17:15:00Z", + "2023-07-03T17:20:00Z", + "2023-07-03T17:25:00Z", + "2023-07-03T17:30:00Z", + "2023-07-03T17:35:00Z", + "2023-07-03T17:40:00Z", + "2023-07-03T17:45:00Z", + "2023-07-03T17:50:00Z", + "2023-07-03T17:55:00Z", + "2023-07-03T18:00:00Z", + "2023-07-03T18:05:00Z", + "2023-07-03T18:10:00Z", + "2023-07-03T18:15:00Z", + "2023-07-03T18:20:00Z", + "2023-07-03T18:25:00Z", + "2023-07-03T18:30:00Z", + "2023-07-03T18:35:00Z", + "2023-07-03T18:40:00Z", + "2023-07-03T18:45:00Z", + "2023-07-03T18:50:00Z", + "2023-07-03T18:55:00Z", + "2023-07-03T19:00:00Z", + "2023-07-03T19:05:00Z", + "2023-07-03T19:10:00Z", + "2023-07-03T19:15:00Z", + "2023-07-03T19:20:00Z", + "2023-07-03T19:25:00Z", + "2023-07-03T19:30:00Z", + "2023-07-03T19:35:00Z", + "2023-07-03T19:40:00Z", + "2023-07-03T19:45:00Z", + "2023-07-03T19:50:00Z", + "2023-07-03T19:55:00Z", + "2023-07-03T20:00:00Z", + "2023-07-03T20:05:00Z", + "2023-07-03T20:10:00Z", + "2023-07-03T20:15:00Z", + "2023-07-03T20:20:00Z", + "2023-07-03T20:25:00Z", + "2023-07-03T20:30:00Z", + "2023-07-03T20:35:00Z", + "2023-07-03T20:40:00Z", + "2023-07-03T20:45:00Z", + "2023-07-03T20:50:00Z", + "2023-07-03T20:55:00Z", + "2023-07-03T21:00:00Z", + "2023-07-03T21:05:00Z", + "2023-07-03T21:10:00Z", + "2023-07-03T21:15:00Z", + "2023-07-03T21:20:00Z", + "2023-07-03T21:25:00Z", + "2023-07-03T21:30:00Z", + "2023-07-03T21:35:00Z", + "2023-07-03T21:40:00Z", + "2023-07-03T21:45:00Z", + "2023-07-03T21:50:00Z", + "2023-07-03T21:55:00Z", + ], + }), + }); + + /** + * up to 164 datapoints(1 hour values) from a {@link Day.energyPerPeriodChannelWithValues} and {@link Day.dataChannelWithValues} + * */ + export const WEEK: OeTester.Types.Channels = ({ + energyChannelWithValues: new QueryHistoricTimeseriesEnergyResponse("0", { + data: {}, + }), + dataChannelWithValues: new QueryHistoricTimeseriesDataResponse("0", { + data: { + "ctrlApiModbusTcp0/Ess0SetActivePowerEquals": [ + 112, + 262, + 392, + 240, + 230, + 229, + 227, + 317, + 224, + 133, + 135, + 133, + 192, + 209, + 90, + 95, + 96, + 164, + 297, + 184, + 182, + 183, + 198, + 333, + 183, + 93, + 97, + 98, + 197, + 266, + 177, + ], + }, + timestamps: [ + "2023-07-01T00:00:00Z", + "2023-07-02T00:00:00Z", + "2023-07-03T00:00:00Z", + "2023-07-04T00:00:00Z", + "2023-07-05T00:00:00Z", + "2023-07-06T00:00:00Z", + "2023-07-07T00:00:00Z", + "2023-07-08T00:00:00Z", + "2023-07-09T00:00:00Z", + "2023-07-10T00:00:00Z", + "2023-07-11T00:00:00Z", + "2023-07-12T00:00:00Z", + "2023-07-13T00:00:00Z", + "2023-07-14T00:00:00Z", + "2023-07-15T00:00:00Z", + "2023-07-16T00:00:00Z", + "2023-07-17T00:00:00Z", + "2023-07-18T00:00:00Z", + "2023-07-19T00:00:00Z", + "2023-07-20T00:00:00Z", + "2023-07-21T00:00:00Z", + "2023-07-22T00:00:00Z", + "2023-07-23T00:00:00Z", + "2023-07-24T00:00:00Z", + "2023-07-25T00:00:00Z", + "2023-07-26T00:00:00Z", + "2023-07-27T00:00:00Z", + "2023-07-28T00:00:00Z", + "2023-07-29T00:00:00Z", + "2023-07-30T00:00:00Z", + "2023-07-31T00:00:00Z", + ], + }), + }); + + + /** + * up to 31 datapoints(1 day values) from a {@link Day.energyPerPeriodChannelWithValues} and {@link Day.dataChannelWithValues}*/ + export const MONTH: OeTester.Types.Channels = { + energyChannelWithValues: new QueryHistoricTimeseriesEnergyResponse("0", { + data: { + "_sum/GridBuyActiveEnergy": 773000, + "_sum/ConsumptionActiveEnergy": 9976102, + "_sum/EssDcChargeEnergy": 3944328, + "_sum/EssDcDischargeEnergy": 3394430, + "_sum/GridSellActiveEnergy": 12738000, + "_sum/ProductionActiveEnergy": 22491000, + }, + }), + energyPerPeriodChannelWithValues: + new QueryHistoricTimeseriesEnergyPerPeriodResponse("0", { + data: { + "_sum/ConsumptionActiveEnergy": [320342, + 346615, + 341433, + 333054, + 358458, + 347872, + 289283, + null, + 556510, + 311366, + 314722, + 355556, + 381671, + 384558, + 366190, + 349336, + 303696, + 288727, + 357434, + 388659, + 402625, + null, + 713771, + 320238, + 332099, + null, + 756429, + 384136, + 371322, + null], + "_sum/EssDcChargeEnergy": [ + 113476, + 162917, + 150189, + 157158, + 149782, + 159833, + 155084, + null, + 228757, + 128138, + 157539, + 59414, + 156504, + 107339, + 156392, + 158925, + 158578, + 121505, + 120971, + 154566, + 173235, + null, + 204273, + 156676, + 143745, + null, + 247673, + 157410, + 104249, + null, + ], + "_sum/EssDcDischargeEnergy": [ + 112818, + 126532, + 139622, + 133212, + 169240, + 98705, + 109367, + null, + 204267, + 118504, + 121261, + 74970, + 144175, + 89897, + 141582, + 111261, + 122274, + 106232, + 139405, + 132225, + 143860, + null, + 235044, + 63914, + 123844, + null, + 242102, + 130546, + 59571, + null, + ], + "_sum/GridBuyActiveEnergy": [ + 16000, + 6000, + 3000, + 3000, + 5000, + 48000, + 4000, + null, + 5000, + 26000, + 17000, + 62000, + 8000, + 66000, + 13000, + 21000, + 4000, + 3000, + 18000, + 27000, + 29000, + null, + 118000, + 85000, + 2000, + null, + 72000, + 28000, + 84000, + null, + ], + "_sum/GridSellActiveEnergy": [ + 603000, + 590000, + 551000, + 572000, + 69000, + 236000, + 626000, + null, + 1003000, + 261000, + 518000, + 698000, + 640000, + 388000, + 471000, + 373000, + 373000, + 677000, + 286000, + 406000, + 249000, + null, + 446000, + 369000, + 558000, + null, + 776000, + 425000, + 574000, + null, + ], + "_sum/ProductionActiveEnergy": [ + 908000, + 967000, + 900000, + 926000, + 403000, + 597000, + 957000, + null, + 1579000, + 556000, + 852000, + 976000, + 1026000, + 724000, + 839000, + 749000, + 709000, + 978000, + 607000, + 790000, + 652000, + null, + 1011000, + 697000, + 908000, + null, + 1466000, + 808000, + 906000, + null, + ], + }, + timestamps: [ + "2023-05-31T22:00:00Z", + "2023-06-01T22:00:00Z", + "2023-06-02T22:00:00Z", + "2023-06-03T22:00:00Z", + "2023-06-04T22:00:00Z", + "2023-06-05T22:00:00Z", + "2023-06-06T22:00:00Z", + "2023-06-07T22:00:00Z", + "2023-06-08T22:00:00Z", + "2023-06-09T22:00:00Z", + "2023-06-10T22:00:00Z", + "2023-06-11T22:00:00Z", + "2023-06-12T22:00:00Z", + "2023-06-13T22:00:00Z", + "2023-06-14T22:00:00Z", + "2023-06-15T22:00:00Z", + "2023-06-16T22:00:00Z", + "2023-06-17T22:00:00Z", + "2023-06-18T22:00:00Z", + "2023-06-19T22:00:00Z", + "2023-06-20T22:00:00Z", + "2023-06-21T22:00:00Z", + "2023-06-22T22:00:00Z", + "2023-06-23T22:00:00Z", + "2023-06-24T22:00:00Z", + "2023-06-25T22:00:00Z", + "2023-06-26T22:00:00Z", + "2023-06-27T22:00:00Z", + "2023-06-28T22:00:00Z", + "2023-06-29T22:00:00Z", + ], + }), + }; + + /** + * up to 12 datapoints(1 month values) from a {@link Day.energyPerPeriodChannelWithValues} and {@link Day.dataChannelWithValues}*/ + export const YEAR: OeTester.Types.Channels = { + energyChannelWithValues: new QueryHistoricTimeseriesEnergyResponse("0", { + data: { + "_sum/GridBuyActiveEnergy": 23209000, + "_sum/ConsumptionActiveEnergy": 58573394, + "_sum/EssDcChargeEnergy": 15296815, + "_sum/EssDcDischargeEnergy": 12898209, + "_sum/GridSellActiveEnergy": 30703000, + "_sum/ProductionActiveEnergy": 68466000, + }, + }), + energyPerPeriodChannelWithValues: + new QueryHistoricTimeseriesEnergyPerPeriodResponse("0", { + data: { + "_sum/ConsumptionActiveEnergy": [11634885, + 8207927, + 8976354, + 8311835, + 10341804, + 9976102, + 975807, + null, + null, + null, + null, + null], + "_sum/EssDcChargeEnergy": [ + 294606, + 1673109, + 3337772, + 3074303, + 2495947, + 3944328, + 372595, + null, + null, + null, + null, + null, + ], + "_sum/EssDcDischargeEnergy": [ + 208491, + 1339036, + 2911126, + 2555138, + 2123751, + 3394430, + 335402, + null, + null, + null, + null, + null, + ], + "_sum/GridBuyActiveEnergy": [9829000, + 4812000, + 2915000, + 2036000, + 2712000, + 773000, + 94000, + null, + null, + null, + null, + null], + "_sum/GridSellActiveEnergy": [20000, + 86000, + 677000, + 3657000, + 12839000, + 12738000, + 627000, + null, + null, + null, + null, + null], + "_sum/ProductionActiveEnergy": [1912000, + 3816000, + 7165000, + 10452000, + 20841000, + 22491000, + 1546000, + null, + null, + null, + null, + null], + }, + timestamps: [ + "2022-12-31T23:00:00Z", + "2023-01-31T23:00:00Z", + "2023-02-28T23:00:00Z", + "2023-03-31T22:00:00Z", + "2023-04-30T22:00:00Z", + "2023-05-31T22:00:00Z", + "2023-06-30T22:00:00Z", + "2023-07-31T22:00:00Z", + "2023-08-31T22:00:00Z", + "2023-09-30T22:00:00Z", + "2023-10-31T23:00:00Z", + "2023-11-30T23:00:00Z", + ], + }), + }; +} diff --git a/ui/src/app/edge/history/Controller/ModbusTcpApi/chart/chart.ts b/ui/src/app/edge/history/Controller/ModbusTcpApi/chart/chart.ts new file mode 100644 index 00000000000..987109ea767 --- /dev/null +++ b/ui/src/app/edge/history/Controller/ModbusTcpApi/chart/chart.ts @@ -0,0 +1,89 @@ +// @ts-strict-ignore +import { Component } from "@angular/core"; +import { TranslateService } from "@ngx-translate/core"; +import { AbstractHistoryChart } from "src/app/shared/components/chart/abstracthistorychart"; +import { ChartAxis, HistoryUtils, YAxisType } from "src/app/shared/service/utils"; +import { ChannelAddress, EdgeConfig } from "src/app/shared/shared"; + +@Component({ + selector: "modbusTcpApiChart", + templateUrl: "../../../../../shared/components/chart/abstracthistorychart.html", +}) +export class ChartComponent extends AbstractHistoryChart { + + public static getChartData(component: EdgeConfig.Component, config: EdgeConfig, chartType: "line" | "bar", translate: TranslateService, showPhases: boolean): HistoryUtils.ChartData { + let writeChannels: string[] = []; + + const colors: string[] = [ + "rgb(191, 144, 33)", + "rgb(162, 191, 33)", + "rgb(86, 191, 33)", + "rgb(33, 191, 165)", + "rgb(33, 115, 191)", + ]; + + const input: HistoryUtils.InputChannel[] = + [{ + name: "SetActivePowerEquals", + powerChannel: ChannelAddress.fromString(component.id + "/Ess0SetActivePowerEquals"), + }]; + + if (component.properties.writeChannels) { + writeChannels = component.properties.writeChannels.filter(c => !c.includes("Ess0SetActivePowerEquals")); + writeChannels.forEach(c => { + input.push({ + name: c, + powerChannel: ChannelAddress.fromString(component.id + `/${c}`), + }); + }); + } + + return { + input, + output: (data: HistoryUtils.ChannelData) => { + const values: HistoryUtils.DisplayValue[] = [{ + name: translate.instant("MODBUS_TCP_API_READ_WRITE.SET_ACTIVE_POWER_EQUALS"), + converter: () => data["SetActivePowerEquals"], + color: "rgb(214, 28, 28)", + }]; + if (writeChannels) { + writeChannels.forEach((c: string, index: number) => { + const name: string = c; + // Add translations for active power channels of ess0 + if (c.includes("Ess0SetActive")) { + const channelName = c.replace("Ess0", ""); + switch (channelName) { + case "SetActivePowerEquals": + return translate.instant("MODBUS_TCP_API_READ_WRITE.SET_ACTIVE_POWER_EQUALS"); + case "SetActivePowerGreaterOrEquals": + return translate.instant("MODBUS_TCP_API_READ_WRITE.SET_ACTIVE_POWER_GREATER_OR_EQUALS"); + case "SetActivePowerLessOrEquals": + return translate.instant("MODBUS_TCP_API_READ_WRITE.SET_ACTIVE_POWER_LESS_OR_EQUALS"); + } + } + + values.push({ + name: name, + converter: () => data[c], + color: colors[index], + }); + }); + } + return values; + }, + tooltip: { + formatNumber: "1.1-2", + }, + yAxes: [{ + unit: YAxisType.ENERGY, + position: "left", + yAxisId: ChartAxis.LEFT, + }, + ], + }; + } + + public override getChartData() { + return ChartComponent.getChartData(this.component, this.config, this.chartType, this.translate, this.showPhases); + } +} diff --git a/ui/src/app/edge/history/Controller/ModbusTcpApi/flat/flat.html b/ui/src/app/edge/history/Controller/ModbusTcpApi/flat/flat.html new file mode 100644 index 00000000000..e7a0c6ad6a6 --- /dev/null +++ b/ui/src/app/edge/history/Controller/ModbusTcpApi/flat/flat.html @@ -0,0 +1,5 @@ + + + + diff --git a/ui/src/app/edge/history/Controller/ModbusTcpApi/flat/flat.ts b/ui/src/app/edge/history/Controller/ModbusTcpApi/flat/flat.ts new file mode 100644 index 00000000000..9a857c71826 --- /dev/null +++ b/ui/src/app/edge/history/Controller/ModbusTcpApi/flat/flat.ts @@ -0,0 +1,21 @@ +import { Component } from "@angular/core"; + +import { AbstractFlatWidget } from "src/app/shared/components/flat/abstract-flat-widget"; +import { ChannelAddress } from "src/app/shared/shared"; + +@Component({ + selector: "modbusTcpApiWidget", + templateUrl: "./flat.html", +}) +export class FlatComponent extends AbstractFlatWidget { + + protected TIME_CONVERTER = this.Converter.FORMAT_SECONDS_TO_DURATION("de"); + + protected override getChannelAddresses(): ChannelAddress[] { + + return [ + new ChannelAddress(this.component.id, "CumulatedInactiveTime"), + new ChannelAddress(this.component.id, "CumulatedActiveTime"), + ]; + } +} diff --git a/ui/src/app/edge/history/Controller/ModbusTcpApi/modbusTcpApi.module.ts b/ui/src/app/edge/history/Controller/ModbusTcpApi/modbusTcpApi.module.ts new file mode 100644 index 00000000000..aef372d5a35 --- /dev/null +++ b/ui/src/app/edge/history/Controller/ModbusTcpApi/modbusTcpApi.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from "@angular/core"; +import { BrowserModule } from "@angular/platform-browser"; +import { SharedModule } from "src/app/shared/shared.module"; +import { FlatComponent } from "./flat/flat"; +import { OverviewComponent } from "./overview/overview"; +import { ChartComponent } from "./chart/chart"; + +@NgModule({ + imports: [ + BrowserModule, + SharedModule, + ], + declarations: [ + FlatComponent, + OverviewComponent, + ChartComponent, + ], + exports: [ + FlatComponent, + OverviewComponent, + ChartComponent, + ], +}) +export class ModbusTcpApi { } diff --git a/ui/src/app/edge/history/Controller/ModbusTcpApi/overview/overview.html b/ui/src/app/edge/history/Controller/ModbusTcpApi/overview/overview.html new file mode 100644 index 00000000000..c08da36f604 --- /dev/null +++ b/ui/src/app/edge/history/Controller/ModbusTcpApi/overview/overview.html @@ -0,0 +1,23 @@ + + +

    + + + + + + + Edge.Index.Widgets.InfoStorageForCharge + + + + + Edge.Index.Widgets.InfoStorageForDischarge + + + + + +
    + + diff --git a/ui/src/app/edge/history/Controller/ModbusTcpApi/overview/overview.ts b/ui/src/app/edge/history/Controller/ModbusTcpApi/overview/overview.ts new file mode 100644 index 00000000000..51ed8f95a67 --- /dev/null +++ b/ui/src/app/edge/history/Controller/ModbusTcpApi/overview/overview.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; +import { AbstractHistoryChartOverview } from "src/app/shared/components/chart/abstractHistoryChartOverview"; + +@Component({ + templateUrl: "./overview.html", +}) +export class OverviewComponent extends AbstractHistoryChartOverview { } diff --git a/ui/src/app/edge/history/Controller/controller.module.ts b/ui/src/app/edge/history/Controller/controller.module.ts index dec242649f6..e009e41d838 100644 --- a/ui/src/app/edge/history/Controller/controller.module.ts +++ b/ui/src/app/edge/history/Controller/controller.module.ts @@ -4,6 +4,7 @@ import { ControllerIo } from "./Io/Io.module"; import { ChannelThreshold } from "./ChannelThreshold/channelThreshold.module"; import { GridOptimizeCharge } from "./Ess/GridoptimizedCharge/gridOptimizeCharge.module"; import { TimeOfUseTariff } from "./Ess/TimeOfUseTariff/timeOfUseTariff.module"; +import { ModbusTcpApi } from "./ModbusTcpApi/modbusTcpApi.module"; @NgModule({ imports: [ @@ -11,6 +12,7 @@ import { TimeOfUseTariff } from "./Ess/TimeOfUseTariff/timeOfUseTariff.module"; ControllerIo, ChannelThreshold, TimeOfUseTariff, + ModbusTcpApi, GridOptimizeCharge, ], exports: [ @@ -18,6 +20,7 @@ import { TimeOfUseTariff } from "./Ess/TimeOfUseTariff/timeOfUseTariff.module"; ControllerIo, ChannelThreshold, TimeOfUseTariff, + ModbusTcpApi, GridOptimizeCharge, ], }) diff --git a/ui/src/app/edge/history/history.component.html b/ui/src/app/edge/history/history.component.html index 8774f678d1b..bd86a09bf04 100644 --- a/ui/src/app/edge/history/history.component.html +++ b/ui/src/app/edge/history/history.component.html @@ -38,6 +38,10 @@ + + + + @@ -72,6 +76,13 @@ + + + + + + + diff --git a/ui/src/app/edge/history/history.component.ts b/ui/src/app/edge/history/history.component.ts index 719a287a158..e2349530d0d 100644 --- a/ui/src/app/edge/history/history.component.ts +++ b/ui/src/app/edge/history/history.component.ts @@ -5,7 +5,7 @@ import { TranslateService } from "@ngx-translate/core"; import { AppService } from "src/app/app.service"; import { HeaderComponent } from "src/app/shared/components/header/header.component"; import { JsonrpcResponseError } from "src/app/shared/jsonrpc/base"; -import { Edge, EdgeConfig, Service, Widgets } from "src/app/shared/shared"; +import { Edge, EdgeConfig, EdgePermission, Service, Widgets } from "src/app/shared/shared"; import { environment } from "src/environments"; @Component({ @@ -34,6 +34,7 @@ export class HistoryComponent implements OnInit { public config: EdgeConfig | null = null; protected errorResponse: JsonrpcResponseError | null = null; + protected isModbusTcpWidgetAllowed: boolean = false; constructor( public service: Service, @@ -44,6 +45,7 @@ export class HistoryComponent implements OnInit { ngOnInit() { this.service.currentEdge.subscribe((edge) => { this.edge = edge; + this.isModbusTcpWidgetAllowed = EdgePermission.isModbusTcpApiWidgetAllowed(edge); }); this.service.getConfig().then(config => { // gather ControllerIds of Channelthreshold Components @@ -74,11 +76,11 @@ export class HistoryComponent implements OnInit { /* handle grid breakpoints */(window.innerWidth < 768 ? window.innerWidth - 150 : window.innerWidth - 400)); this.socChartHeight = /* minimum size */ Math.max(150, - /* maximium size */ Math.min(200, ref), + /* maximum size */ Math.min(200, ref), ) + "px"; this.energyChartHeight = /* minimum size */ Math.max(300, - /* maximium size */ Math.min(600, ref), + /* maximum size */ Math.min(600, ref), ) + "px"; } diff --git a/ui/src/app/edge/live/Controller/ModbusTcpApi/flat/flat.html b/ui/src/app/edge/live/Controller/ModbusTcpApi/flat/flat.html new file mode 100644 index 00000000000..9df8ac9fb01 --- /dev/null +++ b/ui/src/app/edge/live/Controller/ModbusTcpApi/flat/flat.html @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/app/edge/live/Controller/ModbusTcpApi/flat/flat.ts b/ui/src/app/edge/live/Controller/ModbusTcpApi/flat/flat.ts new file mode 100644 index 00000000000..89b23b86751 --- /dev/null +++ b/ui/src/app/edge/live/Controller/ModbusTcpApi/flat/flat.ts @@ -0,0 +1,51 @@ +import { Component } from "@angular/core"; +import { AbstractFlatWidget } from "src/app/shared/components/flat/abstract-flat-widget"; +import { ChannelAddress, CurrentData } from "src/app/shared/shared"; +import { OverrideStatus } from "src/app/shared/type/general"; +import { ModalComponent } from "../modal/modal"; + +@Component({ + selector: "Controller_Api_ModbusTcp", + templateUrl: "./flat.html", +}) +export class FlatComponent extends AbstractFlatWidget { + + protected overrideStatus: OverrideStatus | null = null; + + async presentModal() { + if (!this.isInitialized) { + return; + } + const modal = await this.modalController.create({ + component: ModalComponent, + componentProps: { + component: this.component, + }, + }); + return await modal.present(); + } + + + protected override getChannelAddresses(): ChannelAddress[] { + + return [ + new ChannelAddress(this.component.id, "OverrideStatus"), + ]; + } + + protected override onCurrentData(currentData: CurrentData) { + this.overrideStatus = this.getTranslatedState(currentData.allComponents[this.component.id + "/OverrideStatus"]); + } + + private getTranslatedState(state: OverrideStatus) { + switch (state) { + case OverrideStatus.ACTIVE: + return this.translate.instant("MODBUS_TCP_API_READ_WRITE.OVERRIDING"); + case OverrideStatus.ERROR: + return this.translate.instant("EVCS.error"); + default: + return this.translate.instant("MODBUS_TCP_API_READ_WRITE.NOT_OVERRIDING"); + } + } + +} diff --git a/ui/src/app/edge/live/Controller/ModbusTcpApi/modal/modal.html b/ui/src/app/edge/live/Controller/ModbusTcpApi/modal/modal.html new file mode 100644 index 00000000000..b8468f22958 --- /dev/null +++ b/ui/src/app/edge/live/Controller/ModbusTcpApi/modal/modal.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +
    + +
    + +
    + +
    + {{ 'MODBUS_TCP_API_READ_WRITE.DOWNLOAD_PROTOCOL' | translate }} +
    + +
    diff --git a/ui/src/app/edge/live/Controller/ModbusTcpApi/modal/modal.ts b/ui/src/app/edge/live/Controller/ModbusTcpApi/modal/modal.ts new file mode 100644 index 00000000000..ae11a7a121a --- /dev/null +++ b/ui/src/app/edge/live/Controller/ModbusTcpApi/modal/modal.ts @@ -0,0 +1,102 @@ +// @ts-strict-ignore +import { Component } from "@angular/core"; +import { ProfileComponent } from "src/app/edge/settings/profile/profile.component"; +import { AbstractModal } from "src/app/shared/components/modal/abstractModal"; +import { Converter } from "src/app/shared/components/shared/converter"; +import { ChannelAddress, ChannelRegister, CurrentData } from "src/app/shared/shared"; +import { OverrideStatus } from "src/app/shared/type/general"; + +@Component({ + templateUrl: "./modal.html", +}) +export class ModalComponent extends AbstractModal { + + protected readonly CONVERT_TO_WATT = Converter.POWER_IN_WATT; + + protected writeChannelValues: number[] | null = []; + protected writeChannels: ChannelAddress[] | null = []; + protected overrideStatus: string | null = null; + protected formattedWriteChannels: string[] | null = null; + + protected activePowerEqualsChannel: ChannelAddress | null = null; + protected activePowerEqualsValue: number | null = null; + protected channelRegisters = ChannelRegister; + + private profile = new ProfileComponent(this.service, this.route, null, this.translate); + + protected override getChannelAddresses(): ChannelAddress[] { + this.activePowerEqualsChannel = new ChannelAddress(this.component.id, "Ess0SetActivePowerEquals"); + const writeChannelIds = this.config.components[this.component.id]?.properties.writeChannels || []; + this.writeChannels = writeChannelIds.map(channelId => new ChannelAddress(this.component.id, channelId)); + return [ + ...this.writeChannels, + this.activePowerEqualsChannel, + new ChannelAddress(this.component.id, "OverrideStatus"), + ]; + } + + protected override onIsInitialized(): void { + this.edge.getConfig(this.websocket).subscribe((config) => { + const newChannels = (config.components[this.component.id]?.properties?.writeChannels || []) + .map(channelId => new ChannelAddress(this.component.id, channelId)); + + this.writeChannels = newChannels.filter(channel => !channel.channelId.includes("Ess0SetActivePowerEquals")); + this.getFormatChannelNames(); + this.edge.subscribeChannels(this.websocket, this.component.id, this.writeChannels); + }); + } + + protected getModbusProtocol(componentId: string) { + return this.profile.getModbusProtocol(componentId); + } + + protected override onCurrentData(currentData: CurrentData) { + this.activePowerEqualsValue = this.edge.currentData.value.channel[this.activePowerEqualsChannel!.toString()]; + this.writeChannelValues = this.writeChannels?.map(channel => + this.edge.currentData.value.channel[channel.toString()], + ) || []; + this.overrideStatus = this.getTranslatedState(currentData.allComponents[this.component.id + "/OverrideStatus"]); + } + + protected getTranslatedChannel(channel: ChannelAddress): string { + if (channel.channelId.includes("Ess0SetActive")) { + const channelName = channel.channelId.replace("Ess0", ""); + switch (channelName) { + case "SetActivePowerEquals": + return this.translate.instant("MODBUS_TCP_API_READ_WRITE.SET_ACTIVE_POWER_EQUALS"); + case "SetActivePowerGreaterOrEquals": + return this.translate.instant("MODBUS_TCP_API_READ_WRITE.SET_ACTIVE_POWER_GREATER_OR_EQUALS"); + case "SetActivePowerLessOrEquals": + return this.translate.instant("MODBUS_TCP_API_READ_WRITE.SET_ACTIVE_POWER_LESS_OR_EQUALS"); + } + } + } + + private getTranslatedState(state: OverrideStatus) { + switch (state) { + case OverrideStatus.ACTIVE: + return this.translate.instant("MODBUS_TCP_API_READ_WRITE.OVERRIDING"); + case OverrideStatus.ERROR: + return this.translate.instant("EVCS.error"); + default: + return this.translate.instant("MODBUS_TCP_API_READ_WRITE.NOT_OVERRIDING"); + } + } + + /** + * This method adds the name and register number of the corresponding channel to + * the modal view. It has to be done dynamically since channels can be overwritten in any order. + */ + private getFormatChannelNames(): void { + this.formattedWriteChannels = []; + this.writeChannels.forEach(channel => { + for (const registerName in ChannelRegister) { + if (channel.channelId.includes(registerName)) { + // If channelId is included in ChannelRegister, get key/value e.g. SetActivePowerEquals/706 + const formattedString = `(${registerName}/${ChannelRegister[registerName]})`; + this.formattedWriteChannels.push(formattedString); + } + } + }); + } +} diff --git a/ui/src/app/edge/live/Controller/ModbusTcpApi/modbusTcpApi.module.ts b/ui/src/app/edge/live/Controller/ModbusTcpApi/modbusTcpApi.module.ts new file mode 100644 index 00000000000..69104d3973c --- /dev/null +++ b/ui/src/app/edge/live/Controller/ModbusTcpApi/modbusTcpApi.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from "@angular/core"; +import { BrowserModule } from "@angular/platform-browser"; +import { SharedModule } from "src/app/shared/shared.module"; +import { FlatComponent } from "./flat/flat"; +import { ModalComponent } from "./modal/modal"; + +@NgModule({ + imports: [ + BrowserModule, + SharedModule, + ], + declarations: [ + FlatComponent, + ModalComponent, + ], + exports: [ + FlatComponent, + ], +}) +export class Controller_Api_ModbusTcp { } diff --git a/ui/src/app/edge/live/live.component.html b/ui/src/app/edge/live/live.component.html index edf538ba7ab..88a68dd2777 100644 --- a/ui/src/app/edge/live/live.component.html +++ b/ui/src/app/edge/live/live.component.html @@ -81,6 +81,13 @@
    + + + + + + diff --git a/ui/src/app/edge/live/live.component.ts b/ui/src/app/edge/live/live.component.ts index e3e022542c6..335b7d1fa62 100644 --- a/ui/src/app/edge/live/live.component.ts +++ b/ui/src/app/edge/live/live.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute } from "@angular/router"; import { RefresherCustomEvent } from "@ionic/angular"; import { Subject } from "rxjs"; import { DataService } from "src/app/shared/components/shared/dataservice"; -import { Edge, EdgeConfig, Service, Utils, Websocket, Widgets } from "src/app/shared/shared"; +import { Edge, EdgeConfig, EdgePermission, Service, Utils, Websocket, Widgets } from "src/app/shared/shared"; @Component({ selector: "live", @@ -14,6 +14,7 @@ export class LiveComponent implements OnInit, OnDestroy { public edge: Edge | null = null; public config: EdgeConfig | null = null; public widgets: Widgets | null = null; + protected isModbusTcpWidgetAllowed: boolean = false; private stopOnDestroy: Subject = new Subject(); constructor( @@ -27,6 +28,7 @@ export class LiveComponent implements OnInit, OnDestroy { public ngOnInit() { this.service.currentEdge.subscribe((edge) => { this.edge = edge; + this.isModbusTcpWidgetAllowed = EdgePermission.isModbusTcpApiWidgetAllowed(edge); }); this.service.getConfig().then(config => { this.config = config; diff --git a/ui/src/app/edge/live/live.module.ts b/ui/src/app/edge/live/live.module.ts index e3d7bbc0de3..944e2240b03 100644 --- a/ui/src/app/edge/live/live.module.ts +++ b/ui/src/app/edge/live/live.module.ts @@ -2,21 +2,14 @@ import { NgModule } from "@angular/core"; import { BrowserModule } from "@angular/platform-browser"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { SharedModule } from "./../../shared/shared.module"; -import { Common_Autarchy } from "./common/autarchy/Common_Autarchy"; -import { Common_Consumption } from "./common/consumption/Common_Consumption"; -import { Common_Grid } from "./common/grid/Common_Grid"; -import { Common_Production } from "./common/production/Common_Production"; -import { Common_Selfconsumption } from "./common/selfconsumption/Common_Selfconsumption"; -import { StorageModalComponent } from "./common/storage/modal/modal.component"; -import { StorageComponent } from "./common/storage/storage.component"; import { Controller_ChannelthresholdComponent } from "./Controller/Channelthreshold/Channelthreshold"; import { Controller_ChpSocComponent } from "./Controller/ChpSoc/ChpSoc"; import { Controller_ChpSocModalComponent } from "./Controller/ChpSoc/modal/modal.component"; import { Controller_Ess_FixActivePower } from "./Controller/Ess/FixActivePower/Ess_FixActivePower"; import { Controller_Ess_GridOptimizedCharge } from "./Controller/Ess/GridOptimizedCharge/Ess_GridOptimizedCharge"; import { Controller_Ess_TimeOfUseTariff } from "./Controller/Ess/TimeOfUseTariff/Ess_TimeOfUseTariff"; -import { AdministrationComponent } from "./Controller/Evcs/administration/administration.component"; import { Controller_Evcs } from "./Controller/Evcs/Evcs"; +import { AdministrationComponent } from "./Controller/Evcs/administration/administration.component"; import { Controller_Io_ChannelSingleThresholdComponent } from "./Controller/Io/ChannelSingleThreshold/Io_ChannelSingleThreshold"; import { Controller_Io_ChannelSingleThresholdModalComponent } from "./Controller/Io/ChannelSingleThreshold/modal/modal.component"; import { Controller_Io_FixDigitalOutputComponent } from "./Controller/Io/FixDigitalOutput/Io_FixDigitalOutput"; @@ -24,22 +17,30 @@ import { Controller_Io_FixDigitalOutputModalComponent } from "./Controller/Io/Fi import { Controller_Io_HeatingElement } from "./Controller/Io/HeatingElement/Io_HeatingElement"; import { Controller_Io_HeatpumpComponent } from "./Controller/Io/Heatpump/Io_Heatpump"; import { Controller_Io_HeatpumpModalComponent } from "./Controller/Io/Heatpump/modal/modal.component"; +import { Controller_Api_ModbusTcp } from "./Controller/ModbusTcpApi/modbusTcpApi.module"; import { Controller_Asymmetric_PeakShavingComponent } from "./Controller/PeakShaving/Asymmetric/Asymmetric"; import { Controller_Asymmetric_PeakShavingModalComponent } from "./Controller/PeakShaving/Asymmetric/modal/modal.component"; -import { Controller_Symmetric_PeakShavingModalComponent } from "./Controller/PeakShaving/Symmetric/modal/modal.component"; import { Controller_Symmetric_PeakShavingComponent } from "./Controller/PeakShaving/Symmetric/Symmetric"; -import { Controller_Symmetric_TimeSlot_PeakShavingModalComponent } from "./Controller/PeakShaving/Symmetric_TimeSlot/modal/modal.component"; +import { Controller_Symmetric_PeakShavingModalComponent } from "./Controller/PeakShaving/Symmetric/modal/modal.component"; import { Controller_Symmetric_TimeSlot_PeakShavingComponent } from "./Controller/PeakShaving/Symmetric_TimeSlot/Symmetric_TimeSlot"; -import { DelayedSellToGridComponent } from "./delayedselltogrid/delayedselltogrid.component"; -import { DelayedSellToGridModalComponent } from "./delayedselltogrid/modal/modal.component"; -import { EnergymonitorModule } from "./energymonitor/energymonitor.module"; -import { InfoComponent } from "./info/info.component"; +import { Controller_Symmetric_TimeSlot_PeakShavingModalComponent } from "./Controller/PeakShaving/Symmetric_TimeSlot/modal/modal.component"; import { Io_Api_DigitalInputComponent } from "./Io/Api_DigitalInput/Io_Api_DigitalInput"; import { Io_Api_DigitalInput_ModalComponent } from "./Io/Api_DigitalInput/modal/modal.component"; -import { LiveComponent } from "./live.component"; import { Evcs_Api_ClusterComponent } from "./Multiple/Evcs_Api_Cluster/Evcs_Api_Cluster"; import { EvcsChartComponent } from "./Multiple/Evcs_Api_Cluster/modal/evcs-chart/evcs.chart"; import { Evcs_Api_ClusterModalComponent } from "./Multiple/Evcs_Api_Cluster/modal/evcsCluster-modal.page"; +import { Common_Autarchy } from "./common/autarchy/Common_Autarchy"; +import { Common_Consumption } from "./common/consumption/Common_Consumption"; +import { Common_Grid } from "./common/grid/Common_Grid"; +import { Common_Production } from "./common/production/Common_Production"; +import { Common_Selfconsumption } from "./common/selfconsumption/Common_Selfconsumption"; +import { StorageModalComponent } from "./common/storage/modal/modal.component"; +import { StorageComponent } from "./common/storage/storage.component"; +import { DelayedSellToGridComponent } from "./delayedselltogrid/delayedselltogrid.component"; +import { DelayedSellToGridModalComponent } from "./delayedselltogrid/modal/modal.component"; +import { EnergymonitorModule } from "./energymonitor/energymonitor.module"; +import { InfoComponent } from "./info/info.component"; +import { LiveComponent } from "./live.component"; import { OfflineComponent } from "./offline/offline.component"; @NgModule({ @@ -56,6 +57,7 @@ import { OfflineComponent } from "./offline/offline.component"; Controller_Ess_FixActivePower, Controller_Ess_GridOptimizedCharge, Controller_Io_HeatingElement, + Controller_Api_ModbusTcp, EnergymonitorModule, SharedModule, Controller_Evcs, diff --git a/ui/src/app/index/login.component.ts b/ui/src/app/index/login.component.ts index 146cbd32166..4fa20f9aeff 100644 --- a/ui/src/app/index/login.component.ts +++ b/ui/src/app/index/login.component.ts @@ -8,6 +8,7 @@ import { environment } from "src/environments"; import { Capacitor } from "@capacitor/core"; import { AppService } from "../app.service"; import { AuthenticateWithPasswordRequest } from "../shared/jsonrpc/request/authenticateWithPasswordRequest"; +import { States } from "../shared/ngrx-store/states"; import { Edge, Service, Utils, Websocket } from "../shared/shared"; @Component({ @@ -93,6 +94,7 @@ export class LoginComponent implements OnInit, AfterContentChecked, OnDestroy { */ public doLogin(param: { username?: string, password: string }) { + this.websocket.state.set(States.AUTHENTICATION_WITH_CREDENTIALS); param = LoginComponent.trimCredentials(param.password, param.username); // Prevent that user submits via keyevent 'enter' multiple times diff --git a/ui/src/app/index/shared/loading-screen.html b/ui/src/app/index/shared/loading-screen.html index 2001205da68..481d1773885 100644 --- a/ui/src/app/index/shared/loading-screen.html +++ b/ui/src/app/index/shared/loading-screen.html @@ -1,5 +1,63 @@ - -

    Loading...

    -
    + + + +

    + LOADING_SCREEN.LOADING + + ... +

    +
    + + + +
    + + + + + + + + + + + +
    + +
    +
    + + + + + + + LOADING_SCREEN.SERVER_NOT_ACCESSIBLE + + + + + + + + LOADING_SCREEN.SERVER_NOT_ACCESSIBLE_DESCRIPTION + + + + + + + + + + LOADING_SCREEN.SERVER_NOT_ACCESSIBLE_TRY_RELOADING + + + + + + diff --git a/ui/src/app/index/shared/loading-screen.ts b/ui/src/app/index/shared/loading-screen.ts index 14d5d7b43a5..05322949c51 100644 --- a/ui/src/app/index/shared/loading-screen.ts +++ b/ui/src/app/index/shared/loading-screen.ts @@ -1,38 +1,41 @@ // @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; +import { Component, effect } from "@angular/core"; import { Router } from "@angular/router"; +import { AppStateTracker } from "src/app/shared/ngrx-store/states"; +import { Environment, environment } from "src/environments"; import { Service, Websocket } from "../../shared/shared"; @Component({ selector: "index", templateUrl: "./loading-screen.html", }) -export class LoadingScreenComponent implements OnInit { +export class LoadingScreenComponent { protected readonly spinnerId: string = "IndexComponent"; + protected readonly environment: Environment = environment; + protected backendState: "loading" | "failed" | "authenticated" = "loading"; constructor( public service: Service, public websocket: Websocket, private router: Router, - ) { } + private appStateTracker: AppStateTracker, + ) { - ngOnInit() { - - // TODO add websocket status observable - const interval = setInterval(() => { - this.service.startSpinner(this.spinnerId); - if (this.websocket.status === "online") { - this.service.stopSpinner(this.spinnerId); - this.router.navigate(["/overview"]); - clearInterval(interval); - } - if (this.websocket.status === "waiting for credentials") { - this.service.stopSpinner(this.spinnerId); - this.router.navigate(["/login"]); - clearInterval(interval); + effect(() => { + this.backendState = this.appStateTracker.loadingState(); + switch (this.backendState) { + case "loading": + this.service.startSpinner(this.spinnerId); + break; + case "failed": + this.service.stopSpinner(this.spinnerId); + break; + case "authenticated": + this.appStateTracker.navigateAfterAuthentication(); + break; } - }, 1000); + }); } } diff --git a/ui/src/app/shared/components/chart/abstracthistorychart.html b/ui/src/app/shared/components/chart/abstracthistorychart.html index 0c1225b56f0..a727e42f4dc 100644 --- a/ui/src/app/shared/components/chart/abstracthistorychart.html +++ b/ui/src/app/shared/components/chart/abstracthistorychart.html @@ -11,3 +11,5 @@ [type]="chartType">
    + + diff --git a/ui/src/app/shared/components/chart/abstracthistorychart.ts b/ui/src/app/shared/components/chart/abstracthistorychart.ts index 97d491dc2f4..b30e69af4b8 100644 --- a/ui/src/app/shared/components/chart/abstracthistorychart.ts +++ b/ui/src/app/shared/components/chart/abstracthistorychart.ts @@ -5,7 +5,7 @@ import { ActivatedRoute } from "@angular/router"; import { TranslateService } from "@ngx-translate/core"; import * as Chart from "chart.js"; import annotationPlugin from "chartjs-plugin-annotation"; -import { calculateResolution, ChronoUnit, DEFAULT_NUMBER_CHART_OPTIONS, DEFAULT_TIME_CHART_OPTIONS, isLabelVisible, Resolution, setLabelVisible } from "src/app/edge/history/shared"; +import { ChronoUnit, DEFAULT_NUMBER_CHART_OPTIONS, DEFAULT_TIME_CHART_OPTIONS, Resolution, calculateResolution, isLabelVisible, setLabelVisible } from "src/app/edge/history/shared"; import { QueryHistoricTimeseriesEnergyPerPeriodResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse"; import { DefaultTypes } from "src/app/shared/service/defaulttypes"; import { v4 as uuidv4 } from "uuid"; @@ -311,6 +311,8 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { return translate.instant("Edge.Index.Widgets.Channeltreshold.ACTIVE_TIME_OVER_PERIOD"); case YAxisType.PERCENTAGE: return translate.instant("General.percentage"); + case YAxisType.REACTIVE: + return "var"; case YAxisType.ENERGY: if (chartType == "bar") { return "kWh"; @@ -556,6 +558,7 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { break; case YAxisType.POWER: case YAxisType.ENERGY: + case YAxisType.REACTIVE: case YAxisType.VOLTAGE: case YAxisType.CURRENT: case YAxisType.NONE: @@ -645,6 +648,9 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { tooltipsLabel = "kW"; } break; + case YAxisType.REACTIVE: + tooltipsLabel = "var"; + break; default: tooltipsLabel = ""; break; @@ -724,6 +730,8 @@ export abstract class AbstractHistoryChart implements OnInit, OnDestroy { return "V"; case YAxisType.CURRENT: return "A"; + case YAxisType.REACTIVE: + return "var"; case YAxisType.ENERGY: if (chartType == "bar") { return "kWh"; diff --git a/ui/src/app/shared/components/edge/edgeconfig.spec.ts b/ui/src/app/shared/components/edge/edgeconfig.spec.ts index 8921e4235d6..ec7d23d4a2c 100644 --- a/ui/src/app/shared/components/edge/edgeconfig.spec.ts +++ b/ui/src/app/shared/components/edge/edgeconfig.spec.ts @@ -172,6 +172,18 @@ export namespace DummyConfig { "io.openems.edge.evcs.api.Evcs", ], }; + + export const MODBUS_TCP_READWRITE = { + id: "Controller.Api.ModbusTcp.ReadWrite", + natureIds: [ + "io.openems.edge.common.jsonapi.JsonApi", + "io.openems.edge.common.component.OpenemsComponent", + "io.openems.edge.controller.api.modbus.ModbusTcpApi", + "io.openems.edge.controller.api.modbus.readwrite.ControllerApiModbusTcpReadWrite", + "io.openems.edge.controller.api.Controller", + "io.openems.edge.timedata.api.TimedataProvider", + ], + }; } export namespace Component { @@ -295,6 +307,21 @@ export namespace DummyConfig { }, channels: {}, }); + + export const MODBUS_TCP_READWRITE = (id: string, alias?: string): Component => ({ + id: id, + alias: alias ?? id, + factory: Factory.MODBUS_TCP_READWRITE, + properties: { + invert: false, + modbusUnitId: 5, + type: "PRODUCTION", + writeChannels: [ + "Ess0SetActivePowerEquals", + ], + }, + channels: {}, + }); } } diff --git a/ui/src/app/shared/components/edge/edgeconfig.ts b/ui/src/app/shared/components/edge/edgeconfig.ts index c06af3db4ac..4b12b73aed2 100644 --- a/ui/src/app/shared/components/edge/edgeconfig.ts +++ b/ui/src/app/shared/components/edge/edgeconfig.ts @@ -710,7 +710,7 @@ export namespace PersistencePriority { } } -export module EdgeConfig { +export namespace EdgeConfig { export class ComponentChannel { public readonly type!: "BOOLEAN" | "SHORT" | "INTEGER" | "LONG" | "FLOAT" | "DOUBLE" | "STRING"; public readonly accessMode!: "RO" | "RW" | "WO"; diff --git a/ui/src/app/shared/components/header/header.component.ts b/ui/src/app/shared/components/header/header.component.ts index 0c192f21047..fc97026c4e6 100644 --- a/ui/src/app/shared/components/header/header.component.ts +++ b/ui/src/app/shared/components/header/header.component.ts @@ -63,7 +63,7 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewChecked { const urlArray = url.split("/"); const file = urlArray.pop(); - if (file == "user" || file == "settings" || file == "changelog" || file == "login" || urlArray.length > 3) { + if (file == "user" || file == "settings" || file == "changelog" || file == "login" || file == "index" || urlArray.length > 3) { // disable side-menu; show back-button instead this.enableSideMenu = false; } else { @@ -75,7 +75,7 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewChecked { updateBackUrl(url: string) { // disable backUrl & Segment Navigation on initial 'login' page - if (url === "/login" || url === "/overview") { + if (url === "/login" || url === "/overview" || url === "/index") { this.backUrl = false; return; } diff --git a/ui/src/app/shared/components/modal/modal.module.ts b/ui/src/app/shared/components/modal/modal.module.ts index 57ecf5e89e0..9f6c54a7b5a 100644 --- a/ui/src/app/shared/components/modal/modal.module.ts +++ b/ui/src/app/shared/components/modal/modal.module.ts @@ -26,6 +26,7 @@ import { ModalHorizontalLineComponent } from "./model-horizontal-line/modal-hori PipeModule, ], declarations: [ + HelpButtonComponent, ModalButtonsComponent, ModalInfoLineComponent, ModalLineComponent, @@ -37,6 +38,7 @@ import { ModalHorizontalLineComponent } from "./model-horizontal-line/modal-hori HelpButtonComponent, ], exports: [ + HelpButtonComponent, ModalButtonsComponent, ModalInfoLineComponent, ModalLineComponent, diff --git a/ui/src/app/shared/components/shared/converter.ts b/ui/src/app/shared/components/shared/converter.ts index 136ecff555b..b76ad1ace3f 100644 --- a/ui/src/app/shared/components/shared/converter.ts +++ b/ui/src/app/shared/components/shared/converter.ts @@ -93,6 +93,34 @@ export namespace Converter { Formatter.FORMAT_WATT(value)); }; + /** + * Formats a Energy value as Kilo watt hours [kWh]. + * + * Value 1000 -> "1,00 kWh". + * Value null -> "-". + * + * @param value the power value + * @returns formatted value; '-' for null + */ + export const WATT_HOURS_IN_KILO_WATT_HOURS: Converter = (raw) => { + return IF_NUMBER(raw, value => + Formatter.FORMAT_KILO_WATT_HOURS(value / 1000)); + }; + + /** + * Formats a Energy value as Kilo watt hours [kWh]. + * + * Value 1000 -> "1000 kWh". + * Value null -> "-". + * + * @param value the power value + * @returns formatted value; '-' for null + */ + export const TO_KILO_WATT_HOURS: Converter = (raw) => { + return IF_NUMBER(raw, value => + Formatter.FORMAT_KILO_WATT_HOURS(value)); + }; + export const STATE_IN_PERCENT: Converter = (raw) => { return IF_NUMBER(raw, value => Formatter.FORMAT_PERCENT(value)); diff --git a/ui/src/app/shared/components/shared/formatter.ts b/ui/src/app/shared/components/shared/formatter.ts index 8f2d5df2c8d..798818e17b6 100644 --- a/ui/src/app/shared/components/shared/formatter.ts +++ b/ui/src/app/shared/components/shared/formatter.ts @@ -7,6 +7,11 @@ export namespace Formatter { return formatNumber(value, "de", "1.0-0") + " W"; }; + export const FORMAT_KILO_WATT_HOURS = (value: number) => { + // TODO apply correct locale + return formatNumber(value, "de", "1.0-0") + " kWh"; + }; + export const FORMAT_VOLT = (value: number) => { // TODO apply correct locale return formatNumber(value, "de", "1.0-0") + " V"; diff --git a/ui/src/app/shared/ngrx-store/states.ts b/ui/src/app/shared/ngrx-store/states.ts new file mode 100644 index 00000000000..33d8f0aeb0d --- /dev/null +++ b/ui/src/app/shared/ngrx-store/states.ts @@ -0,0 +1,110 @@ +import { Injectable, WritableSignal, effect, signal } from "@angular/core"; +import { Router } from "@angular/router"; + +import { differenceInSeconds } from "date-fns"; +import { environment } from "src/environments"; +import { Pagination } from "../service/pagination"; +import { PreviousRouteService } from "../service/previousRouteService"; +import { Websocket } from "../shared"; + +export enum States { + WEBSOCKET_CONNECTION_CLOSED, + WEBSOCKET_NOT_YET_CONNECTED, + WEBSOCKET_CONNECTING, + WEBSOCKET_CONNECTED, + + // TODO substates + NOT_AUTHENTICATED, + AUTHENTICATING_WITH_TOKEN, + AUTHENTICATION_WITH_CREDENTIALS, + AUTHENTICATED, + EDGE_SELECTED, +} + +@Injectable({ + providedIn: "root", +}) +export class AppStateTracker { + private static readonly LOG_PREFIX: string = "AppState"; + private static readonly TIME_TILL_TIMEOUT: number = 10; + private static readonly ENABLE_ROUTING: boolean = true; + public loadingState: WritableSignal<"failed" | "loading" | "authenticated"> = signal("loading"); + private lastTimeStamp: Date | null = null; + + constructor( + protected router: Router, + protected pagination: Pagination, + private websocket: Websocket, + private previousRouteService: PreviousRouteService, + ) { + if (!localStorage.getItem("AppState")) { + console.log(`${AppStateTracker.LOG_PREFIX} Log deactivated`); + } + + if (!AppStateTracker.ENABLE_ROUTING) { + console.log(`${AppStateTracker.LOG_PREFIX} Routing deactivated`); + } + + effect(() => { + const state = this.websocket.state(); + this.startStateHandler(state); + }, { allowSignalWrites: true }); + } + + /** + * Handles navigation after authentication + */ + public navigateAfterAuthentication() { + const segments = this.router.routerState.snapshot.url.split("/"); + const previousUrl: string = this.previousRouteService.getPreviousUrl(); + + if ((previousUrl === segments[segments.length - 1]) || previousUrl === "/") { + this.router.navigate(["./overview"]); + return; + } + + this.router.navigate(previousUrl.split("/")); + } + + private startStateHandler(state: States): void { + + if (environment.debugMode && localStorage.getItem("AppState")) { + console.log(`${AppStateTracker.LOG_PREFIX} [${States[this.websocket.state()]}]`); + } + + if (!AppStateTracker.ENABLE_ROUTING) { + return; + } + + switch (state) { + case States.WEBSOCKET_CONNECTING: + this.lastTimeStamp = this.handleWebSocketConnecting(this.lastTimeStamp); + break; + case States.WEBSOCKET_CONNECTION_CLOSED: + break; + case States.AUTHENTICATED: + this.loadingState.set("authenticated"); + break; + default: + this.lastTimeStamp = null; + break; + } + } + + + private handleWebSocketConnecting(lastTimeStamp: Date | null): Date | null { + const now = new Date(); + if (lastTimeStamp === null) { + return now; + } + + if (differenceInSeconds(now, lastTimeStamp) > AppStateTracker.TIME_TILL_TIMEOUT) { + console.warn(`Websocket connection couldnt be established in ${AppStateTracker.TIME_TILL_TIMEOUT}s`); + this.loadingState.set("failed"); + this.router.navigate(["index"]); + return null; + } + + return lastTimeStamp; + } +} diff --git a/ui/src/app/shared/pipe/converter/converter.ts b/ui/src/app/shared/pipe/converter/converter.ts new file mode 100644 index 00000000000..34cc2fc6649 --- /dev/null +++ b/ui/src/app/shared/pipe/converter/converter.ts @@ -0,0 +1,22 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +import { Converter } from "../../components/shared/converter"; + +@Pipe({ + name: "converter", +}) +export class ConverterPipe implements PipeTransform { + + constructor() { } + + /** + * Transforms the value with a given converter + * + * @param value the passed value + * @param converter the passed converter + * @returns the result of the converter as a string + */ + transform(value: number, converter: Converter): string { + return converter(value); + } +} diff --git a/ui/src/app/shared/pipe/pipe.ts b/ui/src/app/shared/pipe/pipe.ts index ff8f05002e8..dba4d592001 100644 --- a/ui/src/app/shared/pipe/pipe.ts +++ b/ui/src/app/shared/pipe/pipe.ts @@ -2,6 +2,7 @@ import { DecimalPipe } from "@angular/common"; import { NgModule } from "@angular/core"; import { BrowserModule } from "@angular/platform-browser"; import { ClassnamePipe } from "./classname/classname.pipe"; +import { ConverterPipe } from "./converter/converter"; import { FormatSecondsToDurationPipe } from "./formatSecondsToDuration/formatSecondsToDuration.pipe"; import { IsclassPipe } from "./isclass/isclass.pipe"; import { KeysPipe } from "./keys/keys.pipe"; @@ -23,6 +24,7 @@ import { VersionPipe } from "./version/version.pipe"; ClassnamePipe, VersionPipe, TypeofPipe, + ConverterPipe, ], exports: [ UnitvaluePipe, @@ -33,6 +35,7 @@ import { VersionPipe } from "./version/version.pipe"; ClassnamePipe, VersionPipe, TypeofPipe, + ConverterPipe, ], providers: [ DecimalPipe, diff --git a/ui/src/app/shared/service/defaulttypes.ts b/ui/src/app/shared/service/defaulttypes.ts index c2551ff2e0b..b01b9875627 100644 --- a/ui/src/app/shared/service/defaulttypes.ts +++ b/ui/src/app/shared/service/defaulttypes.ts @@ -5,7 +5,7 @@ import { endOfMonth, endOfYear, format, getDay, getMonth, getYear, isSameDay, is import { QueryHistoricTimeseriesEnergyResponse } from "../jsonrpc/response/queryHistoricTimeseriesEnergyResponse"; import { ChannelAddress, Service } from "../shared"; -export module DefaultTypes { +export namespace DefaultTypes { export type Backend = "OpenEMS Backend" | "OpenEMS Edge"; diff --git a/ui/src/app/shared/service/globalRouteChangeHandler.ts b/ui/src/app/shared/service/globalRouteChangeHandler.ts index 17d628c4fa7..8992c77e7a4 100644 --- a/ui/src/app/shared/service/globalRouteChangeHandler.ts +++ b/ui/src/app/shared/service/globalRouteChangeHandler.ts @@ -4,6 +4,7 @@ import { Router, RoutesRecognized } from "@angular/router"; import { TranslateService } from "@ngx-translate/core"; import { filter, map } from "rxjs/operators"; +import { environment } from "src/environments"; import { Service } from "./service"; @Injectable({ @@ -35,7 +36,7 @@ export class GlobalRouteChangeHandler { throw new Error("Either use navbarTitle or navbarTitleToBeTranslated"); } - this.service.currentPageTitle = e.navbarTitle ?? (e.navbarTitleToBeTranslated ? translate.instant(e.navbarTitleToBeTranslated) : null) ?? this.service.currentPageTitle; + this.service.currentPageTitle = e.navbarTitle ?? (e.navbarTitleToBeTranslated ? translate.instant(e.navbarTitleToBeTranslated) : null) ?? this.service.currentPageTitle ?? environment.uiTitle; }); } } diff --git a/ui/src/app/shared/service/pagination.ts b/ui/src/app/shared/service/pagination.ts index ed549837116..f75a43c2f71 100644 --- a/ui/src/app/shared/service/pagination.ts +++ b/ui/src/app/shared/service/pagination.ts @@ -4,6 +4,7 @@ import { Router } from "@angular/router"; import { SubscribeEdgesRequest } from "../jsonrpc/request/subscribeEdgesRequest"; import { ChannelAddress, Edge } from "../shared"; import { Service } from "./service"; +import { States } from "../ngrx-store/states"; @Directive() export class Pagination { @@ -22,6 +23,7 @@ export class Pagination { this.edge = edge; this.service.websocket.sendRequest(new SubscribeEdgesRequest({ edges: [edge.id] })); }).then(() => { + this.service.websocket.state.set(States.EDGE_SELECTED); this.edge.subscribeChannels(this.service.websocket, "", [ new ChannelAddress("_sum", "State"), ]); diff --git a/ui/src/app/shared/service/previousRouteService.ts b/ui/src/app/shared/service/previousRouteService.ts new file mode 100644 index 00000000000..f6087a0ea2c --- /dev/null +++ b/ui/src/app/shared/service/previousRouteService.ts @@ -0,0 +1,29 @@ +import { Injectable } from "@angular/core"; +import { NavigationEnd, Router } from "@angular/router"; + +@Injectable() +export class PreviousRouteService { + + private previousUrl: string; + private currentUrl: string; + + constructor(private router: Router) { + this.currentUrl = this.router.url; + this.previousUrl = this.currentUrl; + router.events.subscribe(event => { + if (event instanceof NavigationEnd) { + this.previousUrl = this.currentUrl; + this.currentUrl = event.url; + } + }); + } + + /** + * Gets the previous url, active before this url + * + * @returns the previous url + */ + public getPreviousUrl() { + return this.previousUrl; + } +} diff --git a/ui/src/app/shared/service/service.ts b/ui/src/app/shared/service/service.ts index 67ce8b1a64f..e25fa22fac3 100644 --- a/ui/src/app/shared/service/service.ts +++ b/ui/src/app/shared/service/service.ts @@ -20,6 +20,7 @@ import { GetEdgeResponse } from "../jsonrpc/response/getEdgeResponse"; import { GetEdgesResponse } from "../jsonrpc/response/getEdgesResponse"; import { QueryHistoricTimeseriesEnergyResponse } from "../jsonrpc/response/queryHistoricTimeseriesEnergyResponse"; import { User } from "../jsonrpc/shared"; +import { States } from "../ngrx-store/states"; import { ChannelAddress } from "../shared"; import { Language } from "../type/language"; import { Role } from "../type/role"; @@ -198,6 +199,7 @@ export class Service extends AbstractService { public onLogout() { this.currentEdge.next(null); this.metadata.next(null); + this.websocket.state.set(States.NOT_AUTHENTICATED); this.router.navigate(["/login"]); } diff --git a/ui/src/app/shared/service/utils.ts b/ui/src/app/shared/service/utils.ts index 36f44a37d47..9e237060345 100644 --- a/ui/src/app/shared/service/utils.ts +++ b/ui/src/app/shared/service/utils.ts @@ -632,6 +632,7 @@ export enum YAxisType { RELAY, ENERGY, VOLTAGE, + REACTIVE, CURRENT, TIME, CURRENCY, diff --git a/ui/src/app/shared/service/websocket.ts b/ui/src/app/shared/service/websocket.ts index a79deefeb77..6df2b0d368b 100644 --- a/ui/src/app/shared/service/websocket.ts +++ b/ui/src/app/shared/service/websocket.ts @@ -1,5 +1,5 @@ // @ts-strict-ignore -import { Injectable } from "@angular/core"; +import { Injectable, WritableSignal, signal } from "@angular/core"; import { Router } from "@angular/router"; import { TranslateService } from "@ngx-translate/core"; import { CookieService } from "ngx-cookie-service"; @@ -18,6 +18,7 @@ import { EdgeRpcRequest } from "../jsonrpc/request/edgeRpcRequest"; import { LogoutRequest } from "../jsonrpc/request/logoutRequest"; import { RegisterUserRequest } from "../jsonrpc/request/registerUserRequest"; import { AuthenticateResponse } from "../jsonrpc/response/authenticateResponse"; +import { States } from "../ngrx-store/states"; import { Language } from "../type/language"; import { Pagination } from "./pagination"; import { Service } from "./service"; @@ -40,6 +41,8 @@ export class Websocket implements WebsocketInterface { | "failed" // connection failed = "initial"; + public state: WritableSignal = signal(States.WEBSOCKET_NOT_YET_CONNECTED); + private readonly wsdata = new WsData(); private socket: WebSocketSubject; @@ -68,6 +71,7 @@ export class Websocket implements WebsocketInterface { public login(request: AuthenticateWithPasswordRequest | AuthenticateWithTokenRequest): Promise { return new Promise((resolve) => { this.sendRequest(request).then(r => { + this.state.set(States.AUTHENTICATED); const authenticateResponse = (r as AuthenticateResponse).result; const language = Language.getByKey(localStorage.DEMO_LANGUAGE ?? authenticateResponse.user.language.toLocaleLowerCase()); @@ -202,10 +206,12 @@ export class Websocket implements WebsocketInterface { * Opens a connection using a stored token. Called once by constructor */ private connect() { + this.state.set(States.WEBSOCKET_NOT_YET_CONNECTED); if (this.status != "initial") { return; } // trying to connect + this.state.set(States.WEBSOCKET_CONNECTING); this.status = "connecting"; if (environment.debugMode) { @@ -219,6 +225,7 @@ export class Websocket implements WebsocketInterface { url: environment.url, openObserver: { next: (value) => { + this.state.set(States.WEBSOCKET_NOT_YET_CONNECTED); // Websocket connection is open if (environment.debugMode) { console.info("Websocket connection opened"); @@ -226,6 +233,7 @@ export class Websocket implements WebsocketInterface { const token = this.cookieService.get("token"); if (token) { + this.state.set(States.AUTHENTICATING_WITH_TOKEN); // Login with Session Token this.login(new AuthenticateWithTokenRequest({ token: token })); @@ -233,6 +241,7 @@ export class Websocket implements WebsocketInterface { } else { // No Token -> directly ask for Login credentials + this.state.set(States.NOT_AUTHENTICATED); this.status = "waiting for credentials"; this.router.navigate(["login"]); } @@ -241,10 +250,12 @@ export class Websocket implements WebsocketInterface { closeObserver: { next: (value) => { // Websocket connection is closed. Auto-Reconnect starts. + this.state.set(States.WEBSOCKET_CONNECTION_CLOSED); if (environment.debugMode) { console.info("Websocket connection closed"); } // trying to connect + this.state.set(States.WEBSOCKET_CONNECTING); this.status = "connecting"; }, }, diff --git a/ui/src/app/shared/shared.module.ts b/ui/src/app/shared/shared.module.ts index 1bf1507d696..2ff830daf99 100644 --- a/ui/src/app/shared/shared.module.ts +++ b/ui/src/app/shared/shared.module.ts @@ -31,8 +31,10 @@ import { HistoryDataErrorModule } from "./components/history-data-error/history- import { PercentageBarComponent } from "./components/percentagebar/percentagebar.component"; import { DirectiveModule } from "./directive/directive"; import { ChartOptionsComponent } from "./legacy/chartoptions/chartoptions.component"; +import { AppStateTracker } from "./ngrx-store/states"; import { PipeModule } from "./pipe/pipe"; import { Logger } from "./service/logger"; +import { PreviousRouteService } from "./service/previousRouteService"; import { Service } from "./service/service"; import { Utils } from "./service/utils"; import { Websocket } from "./shared"; @@ -143,11 +145,13 @@ export function SubnetmaskValidatorMessage(err, field: FormlyFieldConfig) { FormlyFieldWithLoadingAnimationComponent, ], providers: [ + AppStateTracker, appRoutingProviders, + Logger, + PreviousRouteService, Service, Utils, Websocket, - Logger, ], }) diff --git a/ui/src/app/shared/shared.ts b/ui/src/app/shared/shared.ts index 4175e96134d..6cf1168edc6 100644 --- a/ui/src/app/shared/shared.ts +++ b/ui/src/app/shared/shared.ts @@ -58,6 +58,10 @@ export class EdgePermission { }, []); } + public static isModbusTcpApiWidgetAllowed(edge: Edge): boolean { + return edge?.isVersionAtLeast("2024.9.1"); + } + /** * Determines if the edge has its channels in the edgeconfig * or if they should be obtained with a separate request. @@ -169,6 +173,15 @@ export enum EssStateMachine { ERROR = 30, } +export enum ChannelRegister { + "SetActivePowerEquals" = 706, + "SetReactivePowerEquals" = 708, + "SetActivePowerLessOrEquals" = 710, + "SetReactivePowerLessOrEquals" = 712, + "SetActivePowerGreaterOrEquals" = 714, + "SetReactivePowerGreaterOrEquals" = 716, +} + /** * Presents a simple */ diff --git a/ui/src/app/shared/type/general.ts b/ui/src/app/shared/type/general.ts index 8394178bfce..8d1d82a528c 100644 --- a/ui/src/app/shared/type/general.ts +++ b/ui/src/app/shared/type/general.ts @@ -17,3 +17,8 @@ export enum WorkMode { TIME = "TIME", NONE = "NONE", } +export enum OverrideStatus { + ACTIVE = 0, + INACTIVE = 1, + ERROR = 2, +} diff --git a/ui/src/app/shared/type/language.ts b/ui/src/app/shared/type/language.ts index 79a88549ab3..4858ceca062 100644 --- a/ui/src/app/shared/type/language.ts +++ b/ui/src/app/shared/type/language.ts @@ -4,8 +4,9 @@ import localES from "@angular/common/locales/es"; import localFR from "@angular/common/locales/fr"; import localJA from "@angular/common/locales/ja"; import localNL from "@angular/common/locales/nl"; -import { TranslateLoader } from "@ngx-translate/core"; +import { TranslateLoader, TranslateService } from "@ngx-translate/core"; import { Observable, of } from "rxjs"; +import { filter, take } from "rxjs/operators"; import cz from "src/assets/i18n/cz.json"; import de from "src/assets/i18n/de.json"; import en from "src/assets/i18n/en.json"; @@ -13,8 +14,9 @@ import es from "src/assets/i18n/es.json"; import fr from "src/assets/i18n/fr.json"; import ja from "src/assets/i18n/ja.json"; import nl from "src/assets/i18n/nl.json"; +import { environment } from "src/environments"; -interface Translation { +export interface Translation { [key: string]: string | Translation; } @@ -107,4 +109,25 @@ export class Language { return lang?.i18nLocaleKey ?? Language.DEFAULT.i18nLocaleKey; } + + /** + * Sets a additional translation file + * + * e.g. AdvertismentModule + * + * @param translationFile the translation file + * @returns translations params + */ + public static async setAdditionalTranslationFile(translationFile: any, translate: TranslateService): Promise<{ lang: string; translations: {}; shouldMerge?: boolean; }> { + const lang = (await translate.onLangChange.pipe(filter(lang => !!lang), take(1)).toPromise()).lang; + let translationKey: string = lang; + if (!(lang in translationFile)) { + + if (environment.debugMode) { + console.warn(`[Advert] No translation available for Language ${lang}. Implemented languages are: ${Object.keys(translationFile)}`); + } + translationKey = Language.EN.key; + } + return { lang: lang, translations: translationFile[translationKey], shouldMerge: true }; + } } diff --git a/ui/src/app/shared/type/widget.ts b/ui/src/app/shared/type/widget.ts index bd838e1641b..5f72e25e0ce 100644 --- a/ui/src/app/shared/type/widget.ts +++ b/ui/src/app/shared/type/widget.ts @@ -1,6 +1,7 @@ // @ts-strict-ignore import { Edge } from "../components/edge/edge"; import { EdgeConfig } from "../components/edge/edgeconfig"; +import { EdgePermission } from "../shared"; export enum WidgetClass { "Energymonitor", @@ -21,6 +22,7 @@ export enum WidgetNature { } export enum WidgetFactory { + "Controller.Api.ModbusTcp.ReadWrite", "Controller.Asymmetric.PeakShaving", "Controller.ChannelThreshold", "Controller.CHP.SoC", @@ -104,6 +106,8 @@ export class Widgets { return config.getComponentIdsByFactory("Controller.ChannelThreshold")?.length > 0; case "Controller_Io_Digital_Outputs": return config.getComponentIdsByFactories("Controller.Io.FixDigitalOutput", "Controller.IO.ChannelSingleThreshold")?.length > 0; + case "Controller.Api.ModbusTcp.ReadWrite": + return EdgePermission.isModbusTcpApiWidgetAllowed(edge); default: return false; } diff --git a/ui/src/assets/i18n/de.json b/ui/src/assets/i18n/de.json index 5e45735b3cf..457f5cb15ab 100644 --- a/ui/src/assets/i18n/de.json +++ b/ui/src/assets/i18n/de.json @@ -858,6 +858,13 @@ }, "CONFIGURATION_MPPT_SELECTION_NOTE": "Weitere Erzeuger können im Anschluss über das App Center konfiguriert werden." }, + "LOADING_SCREEN": { + "LOADING": "Wird geladen", + "SERVER_NOT_ACCESSIBLE": "Server nicht erreichbar", + "SERVER_NOT_ACCESSIBLE_TRY_RELOADING": "Probieren Sie bitte in 10 Minuten den Browser neu zu laden.", + "SERVER_NOT_ACCESSIBLE_ACCESS_LOCALLY": "Lokal zugreifen", + "SERVER_NOT_ACCESSIBLE_DESCRIPTION": "Der Onlinezugriff auf das Speichersystem ist aktuell nicht möglich. Die Daten werden lokal gespeichert und gehen nicht verloren." + }, "Login": { "title": "Login", "preamble": "Bitte geben Sie Ihr Passwort ein oder bestätigen Sie die Voreingabe um sich als Gast anzumelden.", @@ -972,6 +979,24 @@ "MORE_CHANNELS": "Weitere Kanäle hinzufügen", "CHANNEL": "Kanal" }, + "MODBUS_TCP_API_READ_WRITE": { + "CURRENT_STATE": "Aktueller Status", + "CUMULATED_ACTIVE_TIME": "Externe Vorgaben berücksichtigt", + "CUMULATED_INACTIVE_TIME": "Keine externe Vorgabe vorhanden", + "NO_OVERRIDDEN_CHANNELS": "Es wurden noch keine Kanäle überschrieben.", + "NOT_OVERRIDING": "Keine externe Vorgabe vorhanden", + "OVERRIDING": "Externe Vorgabe wird berücksichtigt", + "LAST_COMMAND": "Letzter Schreibbefehl", + "ACTIVE_POWER_LIMITATIONS": "Wirkleistungsvorgabe", + "REGISTER": "Register", + "LIMITATION": "Vorgabe", + "ACTUAL_VALUE": "Tatsächlicher Wert", + "INFO_TEXT": "Tatsächlicher Wert kann von der Vorgabe abweichen. Negative Werte entsprechen Speicherbeladung - postive Speicherentladung. Eine detaillierte Erklärung finden Sie in der Anleitung.", + "SET_ACTIVE_POWER_EQUALS": "Vorgabe Be- bzw. Entladeleistung", + "SET_ACTIVE_POWER_GREATER_OR_EQUALS": "Minimale Beladeleistung", + "SET_ACTIVE_POWER_LESS_OR_EQUALS": "Maximale Beladeleistung", + "DOWNLOAD_PROTOCOL": "Protokoll Herunterladen" + }, "GRID_STATES": { "OFF_GRID": "Netzausfall", "NO_EXTERNAL_LIMITATION": "keine externe Limitierung", diff --git a/ui/src/assets/i18n/en.json b/ui/src/assets/i18n/en.json index 3c555925b24..b3f33f87876 100644 --- a/ui/src/assets/i18n/en.json +++ b/ui/src/assets/i18n/en.json @@ -860,6 +860,13 @@ }, "CONFIGURATION_MPPT_SELECTION_NOTE": "Additional generators can then be configured via the App Center." }, + "LOADING_SCREEN": { + "LOADING": "Loading", + "SERVER_NOT_ACCESSIBLE": "Server not accessible", + "SERVER_NOT_ACCESSIBLE_DESCRIPTION": "Online view of the storage system is currently not possible. The data is saved locally and will not be lost.", + "SERVER_NOT_ACCESSIBLE_TRY_RELOADING": "Please try reloading the browser in 10 minutes.", + "SERVER_NOT_ACCESSIBLE_ACCESS_LOCALLY": "Access locally" + }, "Login": { "title": "Login", "preamble": "Please enter your password or submit the default value to login as a guest.", @@ -975,6 +982,24 @@ "MORE_CHANNELS": "Add More Channels", "CHANNEL": "Channel" }, + "MODBUS_TCP_API_READ_WRITE": { + "CURRENT_STATE": "Current state", + "CUMULATED_ACTIVE_TIME": "External commands considered", + "CUMULATED_INACTIVE_TIME": "No external commands present", + "NO_OVERRIDDEN_CHANNELS": "No channels have been overridden yet.", + "NOT_OVERRIDING": "No external commands present", + "OVERRIDING": "External command is being considered", + "LAST_COMMAND": "Last override", + "ACTIVE_POWER_LIMITATIONS": "Active power limitations", + "REGISTER": "Register", + "LIMITATION": "Limitation", + "ACTUAL_VALUE": "Actual value", + "INFO_TEXT": "Actual value may deviate from the specification. Negative values correspond to storage loading - positive storage discharging. A detailed explanation can be found in the instructions.", + "SET_ACTIVE_POWER_EQUALS": "Limitation charing/discharging power", + "SET_ACTIVE_POWER_GREATER_OR_EQUALS": "Minimum charging power", + "SET_ACTIVE_POWER_LESS_OR_EQUALS": "Maximum charging power", + "DOWNLOAD_PROTOCOL": "Download Protocol" + }, "GRID_STATES": { "OFF_GRID": "Power outage", "NO_EXTERNAL_LIMITATION": "No external limitation", diff --git a/ui/src/themes/openems/scss/variables.scss b/ui/src/themes/openems/scss/variables.scss index 936fd07be47..abf1541b28c 100644 --- a/ui/src/themes/openems/scss/variables.scss +++ b/ui/src/themes/openems/scss/variables.scss @@ -15,6 +15,7 @@ $font-family: var(--ion-font-family); --ion-color-primary-contrast: #000000; --ion-color-primary-contrast-rgb: 0, 0, 0; --ion-color-primary-shade: #cc995d; + --ion-color-primary-shade-rgb: rgb(204, 153, 93); --ion-color-primary-tint: #eab679; /** secondary **/ @@ -106,9 +107,13 @@ $font-family: var(--ion-font-family); } --ion-color-production: #36aed1; - --ion-color-production-rgb: 54, 174, 209; + --ion-color-production-rgb: 54, + 174, + 209; --ion-color-production-contrast: #fff; - --ion-color-production-contrast-rgb: 255, 255, 255; + --ion-color-production-contrast-rgb: 255, + 255, + 255; --ion-color-production-shade: #226e84; --ion-color-production-tint: #41d4ff;