diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000000..a1be5e311ce --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,59 @@ +name: Build OpenEMS +on: [push] +jobs: + build-code: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Setup Java 8 + uses: actions/setup-java@v1 + with: + java-version: '8' + + - name: Setup Cache for Java/Gradle + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: ${{ runner.os }}-gradle- + + - name: Setup Cache for Java/Maven + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: '14' + + - name: Setup Cache for Node.js + uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: ${{ runner.os }}-node- + + - name: Build all Java packages + run: ./gradlew build + + - name: Resolve OpenEMS Backend bundles + run: ./gradlew resolve.BackendApp + + - name: Validate BackendApp.bndrun + run: git diff --exit-code io.openems.backend.application/BackendApp.bndrun + + - name: Resolve OpenEMS Edge bundles + run: ./gradlew resolve.EdgeApp + + - name: Validate EdgeApp.bndrun + run: git diff --exit-code io.openems.edge.application/EdgeApp.bndrun + + - name: Build OpenEMS UI + run: ./gradlew buildUiForEdge --continue diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000000..51f1ff405d1 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,25 @@ +name: Build Docs +on: + push: + branches: + - develop + +jobs: + build-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Build Javadocs + run: ./gradlew buildAggregatedJavadocs --continue + + - name: Build Antora-docs for openems.io + run: ./gradlew buildAntoraDocs --continue + + - name: Deploy to GitHub pages + uses: peaceiris/actions-gh-pages@v3 + with: + personal_token: ${{ secrets.DOCS }} + external_repository: OpenEMS/openems.io + publish_branch: master + publish_dir: build/www diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index 3830ce11316..e83d1c81679 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -6,4 +6,7 @@ RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \ # disable angular analytics ENV NG_CLI_ANALYTICS=false +# Docker build does not rebuild an image when a base image is changed, increase this counter to trigger it. +ENV TRIGGER_REBUILD 3 + RUN npm install -g @angular/cli diff --git a/.gradle-wrapper/gradle-wrapper.properties b/.gradle-wrapper/gradle-wrapper.properties index da9702f9e70..442d9132ea3 100644 --- a/.gradle-wrapper/gradle-wrapper.properties +++ b/.gradle-wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d663fe00f1e..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,31 +0,0 @@ -sudo: false - -matrix: - include: - - language: java - jdk: - - openjdk8 - script: - - ./gradlew build - - ./gradlew resolve.BackendApp - - git diff --exit-code io.openems.backend.application/BackendApp.bndrun - - ./gradlew resolve.EdgeApp - - git diff --exit-code io.openems.edge.application/EdgeApp.bndrun - - ./gradlew buildAggregatedJavadocs --continue - - ./gradlew buildAntoraDocs --continue - deploy: - provider: pages - skip-cleanup: true - github-token: $GITHUB_TOKEN - keep-history: true - on: - branch: develop - repo: OpenEMS/openems.io - target-branch: master - local-dir: build/www - - - language: node_js - node_js: lts/* - script: - - ./gradlew buildUiForEdge --continue - diff --git a/README.md b/README.md index f037d915a8a..cd4c200c366 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://www.travis-ci.com/OpenEMS/openems.svg?branch=develop)](https://www.travis-ci.com/OpenEMS/openems) +[![Build Status](https://github.com/OpenEMS/openems/actions/workflows/build.yml/badge.svg)](https://github.com/OpenEMS/openems/actions/workflows/build.yml) [![Gitpod live-demo](https://img.shields.io/badge/Gitpod-live--demo-blue?logo=gitpod)](https://gitpod.io/#https://github.com/OpenEMS/openems/tree/master) [![Cite via Zenodo](https://zenodo.org/badge/DOI/10.5281/zenodo.4440884.svg)](https://doi.org/10.5281/zenodo.4440883) diff --git a/build.gradle b/build.gradle index ea1a3fdaaa8..3a3dbc25abd 100644 --- a/build.gradle +++ b/build.gradle @@ -69,6 +69,7 @@ task buildAggregatedJavadocs(type: Javadoc, description: 'Generate javadocs from title = "OpenEMS Javadoc" subprojects.each { proj -> proj.tasks.withType(Javadoc).each { javadocTask -> + options.addStringOption('Xdoclint:none', '-quiet') source += javadocTask.source classpath += javadocTask.classpath excludes += javadocTask.excludes diff --git a/cnf/checkstyle.xml b/cnf/checkstyle.xml index 66dbd3c0f71..bd1b163addc 100644 --- a/cnf/checkstyle.xml +++ b/cnf/checkstyle.xml @@ -120,7 +120,7 @@ - + diff --git a/cnf/pom.xml b/cnf/pom.xml index 34624740030..303acaec670 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -81,7 +81,7 @@ com.zaxxer HikariCP - 4.0.1 + 4.0.2 @@ -106,7 +106,7 @@ net.java.dev.jna jna - 5.6.0 + 5.7.0 @@ -211,12 +211,12 @@ org.ops4j.pax.logging pax-logging-api - 2.0.6 + 2.0.8 org.ops4j.pax.logging pax-logging-log4j1 - 2.0.6 + 2.0.8 org.osgi @@ -237,7 +237,7 @@ org.postgresql postgresql - 42.2.18 + 42.2.19 diff --git a/doc/modules/ROOT/assets/images/config-architecture.png b/doc/modules/ROOT/assets/images/config-architecture.png new file mode 100644 index 00000000000..d46de06fa08 Binary files /dev/null and b/doc/modules/ROOT/assets/images/config-architecture.png differ diff --git a/doc/modules/ROOT/assets/images/device-nature-channel-scheduler-controller.png b/doc/modules/ROOT/assets/images/device-nature-channel-scheduler-controller.png index 7faa0e172e8..5a1b729646f 100644 Binary files a/doc/modules/ROOT/assets/images/device-nature-channel-scheduler-controller.png and b/doc/modules/ROOT/assets/images/device-nature-channel-scheduler-controller.png differ diff --git a/doc/modules/ROOT/assets/images/scheduler-ess-priority.png b/doc/modules/ROOT/assets/images/scheduler-ess-priority.png new file mode 100644 index 00000000000..3020a7e311f Binary files /dev/null and b/doc/modules/ROOT/assets/images/scheduler-ess-priority.png differ diff --git a/doc/modules/ROOT/assets/images/ui-component-install-overview.png b/doc/modules/ROOT/assets/images/ui-component-install-overview.png new file mode 100644 index 00000000000..578746511e6 Binary files /dev/null and b/doc/modules/ROOT/assets/images/ui-component-install-overview.png differ diff --git a/doc/modules/ROOT/assets/images/ui-component-install.png b/doc/modules/ROOT/assets/images/ui-component-install.png new file mode 100644 index 00000000000..01226bcbcb5 Binary files /dev/null and b/doc/modules/ROOT/assets/images/ui-component-install.png differ diff --git a/doc/modules/ROOT/assets/images/ui-config.png b/doc/modules/ROOT/assets/images/ui-config.png deleted file mode 100644 index 4431eb29176..00000000000 Binary files a/doc/modules/ROOT/assets/images/ui-config.png and /dev/null differ diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 8c0ed173cd3..4e8e281810a 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -13,10 +13,10 @@ ** xref:edge/build.adoc[Build OpenEMS Edge] ** xref:edge/deploy.adoc[Deploy OpenEMS Edge] * OpenEMS UI +** xref:ui/architecture.adoc[Architecture] ** xref:ui/build.adoc[Build OpenEMS UI] * OpenEMS Backend ** xref:backend/architecture.adoc[Architecture] -** xref:backend/configuration.adoc[Configuration] ** xref:backend/backend-to-backend.adoc[Backend-to-Backend] ** xref:backend/build.adoc[Build OpenEMS Backend] ** xref:backend/deploy.adoc[Deploy OpenEMS Backend] diff --git a/doc/modules/ROOT/pages/backend/configuration.adoc b/doc/modules/ROOT/pages/backend/configuration.adoc deleted file mode 100644 index b04efd4e2a8..00000000000 --- a/doc/modules/ROOT/pages/backend/configuration.adoc +++ /dev/null @@ -1,2 +0,0 @@ -= Configuration -:imagesdir: ../../assets/images \ No newline at end of file diff --git a/doc/modules/ROOT/pages/component-communication/index.adoc b/doc/modules/ROOT/pages/component-communication/index.adoc index 0017beaba17..57f0bb56558 100644 --- a/doc/modules/ROOT/pages/component-communication/index.adoc +++ b/doc/modules/ROOT/pages/component-communication/index.adoc @@ -9,22 +9,163 @@ :imagesdir: ../../assets/images -This page describes the internal communication protocol between OpenEMS Edge, OpenEMS Backend and OpenEMS UI. The components keep an open https://de.wikipedia.org/wiki/WebSocket[Websocket] connection which is used for bi-directional communication. The protocol is based on https://www.jsonrpc.org/specification[JSON-RPC]. For details about JSON-RPC please refer to the specification. As a rough summary, the protocol is divided into +This page describes the internal communication protocol between OpenEMS Edge, OpenEMS Backend and OpenEMS UI. The components keep an open https://de.wikipedia.org/wiki/WebSocket[Websocket] connection which is used for bi-directional communication. -JSON-RPC Request:: - Identified by a unique ID and method name with specific params. Expects a Response. +== JSON-RPC introduction -JSON-RPC Success Response:: - Referring to the ID of the Request, providing a result which can be empty or hold specific data. +The protocol is based on https://www.jsonrpc.org/specification[JSON-RPC]. For details about JSON-RPC please refer to the specification. As a rough summary, the protocol is divided into -JSON-RPC Error Response:: - Referring to the ID of the Request, providing error code, message and optionally data. +=== JSON-RPC Request -JSON-RPC Notification:: - Identified by a unique method name with specific params. Does not expect a Response. +Identified by a unique ID and method name with specific params. Expects a Response. + +[source,json] +---- +{ + "jsonrpc": "2.0", + "id": UUID, + "method": "method", + "params": {} +} +---- + +=== JSON-RPC Success Response + +Referring to the ID of the Request, providing a result which can be empty or hold specific data. + +[source,json] +---- +{ + "jsonrpc": "2.0", + "id": UUID, + "result": {} +} +---- + +=== JSON-RPC Error Response + +Referring to the ID of the Request, providing error code, message and optionally data. + +[source,json] +---- +{ + "jsonrpc": "2.0", + "id": UUID, + "result": { + "code": number, + "message": string, + "data"?: {} + } +} +---- + +=== JSON-RPC Notification + +Identified by a unique method name with specific params. Does not expect a Response. + +[source,json] +---- +{ + "jsonrpc": "2.0", + "method": "method", + "params": {} +} +---- + +== Example communication messages The information on this page is useful to understand the internal communication structure and can help if your plan is to replace one component by a custom implementation, e.g. implementing your own frontend application, or if you plan to extend the provided feature-set. +== Subscribe Channels + +Real-time channel data may change at a high rate. Instead of requiring the consumer to constantly pull the data, the OpenEMS API provides a publish-subscribe schema that notifies the consumer about updated values. It is initiated via a JSON-RPC request: + +[source,json] +---- +{ + "jsonrpc": "2.0", + "id": UUID, + "method": "subscribeChannels", + "params": { + "count": number, + "channels": string[] + } +} +---- + +The parameters for subscribing to channel data are: + +- `count`: a request count value that needs to be increased on each request to avoid concurrency problems +- `channels`: a list of channel addresses as strings, e.g. "ess0/Soc", "ess0/ActivePower" + +From then on, the API constantly keeps sending `currentData` JSON-RPC notifications, containing the data for all subscribed channels: +[source,json] +---- +{ + "jsonrpc": "2.0", + "method": "currentData", + "params": { + [channelAddress]: string | number + } +} +---- + +To unsubscribe from channels, a new `subscribeChannels` request has to be sent with an empty list of channels. + + +== Edge-RPC + +When using the API via OpenEMS Backend, it is possible to transparently target a specific OpenEMS Edge, that is connected to the Backend by wrapping the JSON-RPC request into an `Edge-RPC` request: + +[source,json] +---- +{ + "jsonrpc": "2.0", + "id": UUID, + "method": "edgeRpc", + "params": { + "edgeId": string, + "payload": JSON-RPC-Request + } +} +---- + +The parameters for an “edgeRpc” request are: + +- `edgeId`: the unique ID of the Edge at the Backend +- `payload`: the JSON-RPC Request that should be forwarded to the Edge, e.g. `getEdgeConfig` or `updateComponentConfig`. + +The JSON-RPC response then also wraps the original result as a payload: + +[source,json] +---- +{ + "jsonrpc": "2.0", + "id": UUID, + "result": { + "payload": JSON-RPC-Response + } +} +---- + +== JsonApi Component + +To directly send a JSON-RPC request to one specific OpenEMS Component, that component has to implement the `JsonApi` interface. +Then the `componentJsonApi` request can be used to wrap a JSON-RPC request as payload: + +[source,json] +---- +{ + "jsonrpc": "2.0", + "id": "UUID", + "method": "componentJsonApi", + "params": { + "componentId": string, + "payload": JSON-RPC-Request + } +} +---- + == Authenticate UI to Edge/Backend using token NOTE: On opening of the websocket connection to Edge/Backend, the UI is authenticated using an existing token. diff --git a/doc/modules/ROOT/pages/coreconcepts.adoc b/doc/modules/ROOT/pages/coreconcepts.adoc index 7abedeec225..f7a194ee617 100644 --- a/doc/modules/ROOT/pages/coreconcepts.adoc +++ b/doc/modules/ROOT/pages/coreconcepts.adoc @@ -56,41 +56,67 @@ for central singleton services: == OpenEMS Component -OpenEMS Edge is built of Components, i.e. every main component implements the link:https://github.com/OpenEMS/openems/blob/develop/io.openems.edge.common/src/io/openems/edge/common/component/OpenemsComponent.java[OpenemsComponent interface icon:code[]]. +An OpenEMS Component is the fundamental building block in OpenEMS. Within the used OSGi Java framework, an OpenEMS component represents a service with requirements and capabilities. + +As an example, an OpenEMS Component can declare to have the capabilities of an Energy Storage System (ESS) and as such represents the digital twin of a real device. +A specific control algorithm can be implemented as a separate OpenEMS Component that declares a requirement for an ESS. +Using this metadata, these building blocks are wired together at runtime and form a very flexible system. +OSGi provides the capability to enable, modify or disable an OpenEMS Component at any time, without requiring a restart of the software. +Re-wiring of the building blocks happens transparently in the background by the framework. + +Every OpenEMS Component is identified by a unique ID, the "Component-ID". +In an ecosystem consisting of a couple of ESS, a power meter at the grid connection point, and a measured photovoltaic system, those Component-IDs can be represented as follows: +* `ess1` for the first ESS +* `ess2` for the second ESS +* `ess0` for a virtual ESS cluster Component that aggregates ess1 and ess2 +* `meter0` for the power meter at the grid connection point +* `meter1` for the measured photovoltaic system +* ... -By definition each Component has a unique ID. Those *Component-IDs* are typically: +To declare an OpenEMS component, the Java class has to `implement` the link:https://github.com/OpenEMS/openems/blob/develop/io.openems.edge.common/src/io/openems/edge/common/component/OpenemsComponent.java[OpenemsComponent interface icon:code[]]. -* `ess0` for the first storage system or battery inverter -* `ess1` for the second storage system or battery inverter -* ... -* `meter0` for the first meter in the system -* ... +== Channel -If you receive your OpenEMS together with a FENECON energy storage system, you will find the following Component-IDs: +Each OpenEMS component has a defined set of data points. +These data points are called "Channels". +Each represents a single piece of information about a component. +By definition, each channel has a unique ID, the "Channel-ID", within its parent component. +Channels are defined by metadata like descriptive text, access-mode (`read-only`, `read-write`, `write-only`), data type (`string`, `integer`, `float`, etc.), and unit of measure (`Watt`, `Volt`, `Degree Celsius`, etc.). +It is up to the OpenEMS component to provide the input for its read-channels as well as triggering actions on write-channels. -* FENECON Pro -** `ess0`: FENECON Pro Ess -// TODO link:https://github.com/OpenEMS/openems/blob/develop/edge/src/io/openems/impl/device/pro/FeneconProEss.java[FENECON Pro Ess icon:code[]] -** `meter0`: Socomec grid meter -// TODO link:https://github.com/OpenEMS/openems/blob/develop/edge/src/io/openems/impl/device/socomec/SocomecMeter.java[Socomec grid meter icon:code[]] -** `meter1`: FENECON Pro production meter -// TODO link:https://github.com/OpenEMS/openems/blob/develop/edge/src/io/openems/impl/device/pro/FeneconProPvMeter.java[FENECON Pro production meter icon:code[]] +_Example:_ An OpenEMS Component that represents a device connected via the Modbus communication protocol continuously reads data, such as the current measured power and provides the data in its Channels. +Other Components in the system can then use the channel data for their application, e.g. as input for a control algorithm, to analyse it, store it locally or publish it via an application programming interface (API). -* FENECON Mini -** `ess0`: FENECON Mini -// TODO link:https://github.com/OpenEMS/openems/blob/develop/edge/src/io/openems/impl/device/minireadonly/FeneconMiniEss.java[FENECON Mini icon:code[]] -** `meter0`: FENECON Mini grid meter -// TODO link:https://github.com/OpenEMS/openems/blob/develop/edge/src/io/openems/impl/device/minireadonly/FeneconMiniGridMeter.java[FENECON Mini grid meter icon:code[]] -** `meter1`: FENECON Mini production meter -// TODO link:https://github.com/OpenEMS/openems/blob/develop/edge/src/io/openems/impl/device/minireadonly/FeneconMiniProductionMeter.java[FENECON Mini production meter icon:code[]] +An energy system architecture as depicted in the Introduction is complex: connected to multiple hardware devices - batteries, converters, meters, and others - and an operating system and other software components. +All of these elements are possible sources of errors. +Because of this, measures are implemented in OpenEMS to improve fault tolerance. +The developer needs to be aware, that every Channel value, while it will never change within a cycle, it could always be `undefined` or `null`, e.g. because there is no communication (yet) with the external hardware device or service. +Therefore, the programming API for accessing a channel value requires an explicit declaration of what should be done in that case. +It provides the following methods to get the actual value: -== Channel +- `public T getOrError() throws InvalidValueException;` +- `public T orElse(T alternativeValue);` -Each OpenemsComponent provides a number of Channels. Each represents a single piece of information. Each Channel implements the link:https://github.com/OpenEMS/openems/blob/develop/io.openems.edge.common/src/io/openems/edge/common/channel/Channel.java[Channel interface icon:code[]]. By definition each Channel has a unique ID within its parent Component. +Each Channel implements the link:https://github.com/OpenEMS/openems/blob/develop/io.openems.edge.common/src/io/openems/edge/common/channel/Channel.java[Channel interface icon:code[]]. == Nature -Natures extend normal Java interfaces with 'Channels'. If a Component implements a Nature it also needs to provide the required Channels. For example the Energy Storage System (ESS) Simulator link:https://github.com/OpenEMS/openems/blob/develop/io.openems.edge.simulator/src/io/openems/edge/simulator/ess/symmetric/reacting/EssSymmetric.java[Simulator.EssSymmetric.Reacting icon:code[]] implements the link:https://github.com/OpenEMS/openems/blob/develop/io.openems.edge.ess.api/src/io/openems/edge/ess/api/SymmetricEss.java[Ess interface icon:code[]] and therefor needs to provide a `Soc` Channel that provides the current 'State of Charge' of the battery. +Certain categories of devices and services provide the same kind of information (i.e. Channels). +To group these similar devices and services, OpenEMS defines "Natures" as sets of characteristics and attributes which need to be provided by each component that implements them. +That is, a Nature extends a normal Java interface with channels. + +Examples of abstracting physical devices using Natures are: +- "SymmetricMeter" for power meters +- "SymmetricEss" for symmetric battery energy storage systems +- "Evcs" for electric vehicle charging stations. + +OpenEMS components can declare their service capabilities and requirements as Natures. +In this way, a control algorithm can simply declare a requirement for a controllable energy storage system (“ManagedSymmetricEss”) and will at runtime be wired with a service that provides this capability. +The control algorithm does not need to know anything about the ESS's specific communication interface, protocol, or manufacturer. + +Natures extend normal Java interfaces with 'Channels'. +If a Component implements a Nature it also needs to provide the required Channels. +For example the Energy Storage System (ESS) Simulator link:https://github.com/OpenEMS/openems/blob/develop/io.openems.edge.simulator/src/io/openems/edge/simulator/ess/symmetric/reacting/EssSymmetric.java[Simulator.EssSymmetric.Reacting icon:code[]] implements the link:https://github.com/OpenEMS/openems/blob/develop/io.openems.edge.ess.api/src/io/openems/edge/ess/api/SymmetricEss.java[Ess interface icon:code[]] and therefor needs to provide a `Soc` Channel that provides the current 'State of Charge' of the battery. xref:edge/controller.adoc[Controllers] are written against Nature implementations. Example: A Controller can be used with any ESS, because it can be sure that it provides all the data the Controller requires for its algorithm. diff --git a/doc/modules/ROOT/pages/edge/architecture.adoc b/doc/modules/ROOT/pages/edge/architecture.adoc index 3befc730cb1..eb4296f3177 100644 --- a/doc/modules/ROOT/pages/edge/architecture.adoc +++ b/doc/modules/ROOT/pages/edge/architecture.adoc @@ -14,6 +14,25 @@ It was developed around the requirements of controlling, monitoring and integrat The OpenEMS Edge software architecture is carefully designed to abstract device communication and control algorithms in a way to provide maximum flexibility, predictability and stability, while simplifying the process of implementing new components. +== High-Level programming language + +OpenEMS Edge and Backend are implemented in the Java programming language and requires a Java Runtime Environment (JRE). This allows convenient development on a personal laptop on any operating system. For productive use, the software typically runs on an Industrial IoT Gateway or a development board like a Raspberry Pi with GNU/Linux Operating System. + +The usage of a high-level programming language for an EMS leads to a trade-off between easy and efficient software development and loss of hard real-time capabilities. + +An Energy Management System collects input data, like measured grid power and state of charge of a battery, and processes it with its control algorithms to derive setpoints which are sent to the hardware devices. (see "Input-Process-Output" below). + +The possible and feasible speed of this execution cycle depends on the performance of the connected devices and the communication paths. +It also means that the EMS has to deal with multi-threading, asynchronous communication and latencies. + +Example: + +- A power smoothing algorithm needs to process the current output power of a photovoltaics system. Most external power meters provide measurements approximately only once every second. In this scenario it is not feasible to run the execution cycle more often than once every second. + +- For control algorithms that require high-performance and minimum delay between measurement and action, e.g. providing Virtual Inertia Ancillary Services, the EMS cycle duration is not sufficient and soft real-time behaviour is not suitable. The same point applies for critical safety measures like fire extinguishing and disaster control measures." + +- Due to the asynchronous communication, new data can arrive at every moment, e.g. the value for active power received by the meter can change at any time between the operation of two consecutive lines of code. The EMS needs to provide measures to avoid errors arising from this multi-threading. + == Input-Process-Output OpenEMS Edge is built around the well-known IPO (input-process-output) model which defines the internal execution cycle. @@ -30,15 +49,58 @@ The process phase runs algorithms and tasks based on the process image - e.g. an Output:: The output phase takes the results from the process phase and applies it - e.g. it turns the digital output on or off. -== Scheduler and Controller +== Controller + +Controllers are consumers of Channel data and hold the actual business logic, e.g. the control algorithm that evaluates input data and defines setpoints for the controlled hardware. + +Examples for controllers are: + +- `Controller.Ess.LimitTotalDischarge` maintains a minimum battery level +- `Controller.Backend.Api` connects to the OpenEMS Backend server +- `Controller.Rest.Api` provides a JSON/REST-Api for external access +- `Controller.Debug.Log` logs regular system status messages to the standard output +- `Controller.Ess.PeakShaving` charges or discharges an ESS in order to cut power peaks at the PCC +- `Controller.Ess.Balancing` charges or discharges an ESS in order to optimize self-consumption from a local photovoltaics system. + +Ideally controller implementations follow the KISS (Keep It Simple Stupid) principle, which means that they carry out only one specific, encapsulated task. +This approach allows very flexible system architectures and avoids duplicated code. +For example, both `Ess.PeakShaving` and `Ess.Balancing` controllers do not need to repeat any logic for keeping the battery at a safe state, as this is what the `Ess.LimitTotalDischarge` controller is responsible for. -During the 'process' phase different algorithms (Controllers) might try to access the same resources - e.g. two Controllers try to switch the same digital output. It is therefore necessary to prioritize their execution and restrict access according to priority. +As can be seen above, controllers are not necessarily restricted to control algorithms. +Even northbound connections to a backend server or SCADA system and alike are implemented as Controllers. +This assures for any setpoint request by an external system being embedded in the local prioritization system and naturally restricted by higher-priority controllers. -OpenEMS Edge uses Scheduler implementations to receive a sorted list of Controllers. The Controllers are then executed in order. Later executed Controllers are not allowed to overwrite a previously written result. +_Example:_ An external request to discharge the battery will be limited by the `Ess.LimitTotalDischarge` controller just like any other internal control algorithm. + +Controllers are executed regularly e.g. once per second (see "Cycle" below). + +== Scheduler + +During the 'process' phase different algorithms (Controllers) might try to access the same resources - e.g. two Controllers try to switch the same digital output. +It is therefore necessary to prioritize their execution and restrict access according to priority. +OpenEMS Edge uses Scheduler implementations to receive a sorted list of Controllers. +The Controllers are then executed in order. +Later executed Controllers are not allowed to overwrite a previously written result. .IPO model with Scheduler and Controllers image::input-process-scheduler-output.png[IPO model with Scheduler and Controllers] +_Example:_ + +In the example of energy storage system, the following figure shows, how the interval of possible solutions is reduced by sequentially executed Controllers. +In the example the initial ESS limits from battery and converter allow charging and discharging with `50 kW`. +The 'Limit Total Discharge' controller then adds a constraint to force charge the ESS, i.e. enforcing a setpoint that is smaller than `-5 kW`. +No further limitations are applied by the Api controllers. +The 'Balancing' controller then requests discharging the ESS with `20 kW` but is forced to fulfil the constraints. +Eventually the ESS gets force-charged with `5 kW`. + +The scheduler in OpenEMS Edge handles this prioritization and sequential execution of controllers. +In the example of controlling an ESS, a separate `Ess.Power` component synchronizes with the IPO cycle and manages feasible solutions via a linear equation system that allows constraints on three-phase or single-phase setpoints for active and reactive power. +It is also used for optimizing distribution of setpoints in the case of multiple ESS. + +.Prioritization of ESS power setpoints +image::scheduler-ess-priority.png[Prioritization of ESS power setpoints] + == Cycle The input-process-output model in OpenEMS Edge is executed in a Cycle - implemented by the link:https://github.com/OpenEMS/openems/blob/develop/io.openems.edge.core/src/io/openems/edge/core/cycle/Cycle.java[Cycle component icon:code[]]. It handles the setting of a process image in the input phase and executes the Controllers in the process phase. Furthermore it emits Cycle Events that can be used in other Components to synchronize with the Cycle. @@ -46,6 +108,20 @@ The input-process-output model in OpenEMS Edge is executed in a Cycle - implemen .OpenEMS Edge Cycle image::edge-cycle.png[OpenEMS Edge Cycle] +== Process Image + +Due to asynchronous communication with external devices and services, data can potentially be updated or invalidated at any point in time. +This could lead to confusing situations, e.g. where a Channel value changes between two consecutive controllers that act on its data. +To avoid these situations and relieve the programmer from taking care of all kinds of concurrency problems, OpenEMS uses a "Process Image", a technique well proven in the field of PLC programming. +The idea is to untie the producers and consumers of data and introducing a central buffer for all channel data. This buffer - the Process Image - is updated only once in every computing cycle when it activates the latest data in each Channel. + +Therefore, the implementation of channel objects in OpenEMS has two data variables: +- The `value` field keeping the currently active value that should be used by consumers +- The `nextValue` field representing the latest data that was received, e.g. via Modbus communication. + +At - and only at - 'Switch Process Image' of the Cycle, the `nextValue` gets copied to the `value` field. +This assures, that the data in the Process Image does not change during a computing Cycle. + == Asynchronous threads and Cycle synchronization Communication with external hardware and services needs to be executed in asynchronous threads to not block the system. At the same time, those threads need to synchronize with the Cycle. @@ -57,7 +133,9 @@ image::cycle-modbus.png[Synchronize Cycle with Modbus read/write] == Architecture scheme +The OpenEMS Edge software architecture is carefully designed to abstract device communication and control algorithms in a way to provide maximum flexibility, predictability and stability while simplifying the process of implementing new components. + The following scheme shows the abstraction of hardware via Channels, Natures and Devices as well as the execution of control algorithms via Scheduler and Controllers. .Architecture scheme -image::device-nature-channel-scheduler-controller.png[Architecture scheme] +image::device-nature-channel-scheduler-controller.png[Architecture scheme] \ No newline at end of file diff --git a/doc/modules/ROOT/pages/edge/configuration.adoc b/doc/modules/ROOT/pages/edge/configuration.adoc index b723169dcb0..b5c5bd54dca 100644 --- a/doc/modules/ROOT/pages/edge/configuration.adoc +++ b/doc/modules/ROOT/pages/edge/configuration.adoc @@ -9,12 +9,204 @@ :icons: font :imagesdir: ../../assets/images -OpenEMS Edge and Backend are configured using the standard OSGi configuration admin service. The easiest way to set a configuration is via the http://localhost:8080/system/console/configMgr[Apache Felix Web Console Configuration icon:external-link[]] as described in the xref:gettingstarted.adoc[Getting Started] guide above. +The actual hardware setup behind an Energy Management System (EMS) usually varies from site to site. +To comply with this, the EMS needs a static, local configuration that declares available hardware components and services and activated control algorithms with their parameters. +This configuration persists locally on the EMS hardware on a storage like an on-board eMMC flash drive or an external SD card. +To reduce writes to local storage and avoid hardware defects, the configuration should not be changed frequently. + +Example: + +- "Power meter of type SOCOMEC Diris A40, measuring the power at the grid connection point is connected on interface /dev/ttySC0 via RS485 bus with baudrate 9600. It listens on Modbus Unit-ID 5" +- "Battery inverter of type KACO 50, connected via Modbus/TCP on IP address 10.4.0.15, port 502" +- "Algorithm for using an energy storage system to apply power smoothing of a photovoltaics installation to a maximum defined ramp rate. Discharge battery on suddenly reduced DRES power; recharge on suddenly increased DRES power." + +## Manage configuration + +OpenEMS Edge and Backend are configured using the standard OSGi https://docs.osgi.org/specification/osgi.cmpn/7.0.0/service.cm.html[Configuration Admin Service]. There are multiple ways to manage this configuration: + +### Via OpenEMS UI + +Via the OpenEMS UI it is possible to configure an OpenEMS Edge that is connected directly or via an OpenEMS Backend: + +.OpenEMS UI Install Components +image::ui-component-install-overview.png[OpenEMS UI Install Components Overview] + +.OpenEMS UI Install Component +image::ui-component-install.png[OpenEMS UI Install Component] + +### Via Apache Felix Web Console + +The 'native way' to manage an OSGi configuration is via the Apache Felix Web Console. By default it listens on port 8080 and can be accessed via http://localhost:8080/system/console/configMgr as described in the xref:gettingstarted.adoc[Getting Started] guide. .Apache Felix Web Console Configuration image::apache-felix-console-configuration.png[Apache Felix Web Console Configuration] -Configuration via OpenEMS UI is currently not available due to the ongoing migration to xref:coreconcepts.adoc#_osgi_bundle[OSGi]. Once migration is finished, it is going to be possible to change every configuration using the settings menu in OpenEMS UI - directly to OpenEMS Edge and via Backend. +### Via JSON-RPC + +The JSON-RPC protocol is used throughout the project to enable access to functions directly on the OpenEMS Edge or via an OpenEMS Backend. See xref:../component-communication/index.adoc[Component Communication] for details. Configuration may be adjusted using the following JSON-RPC methods: + +[source,json] +---- +{ + "method": "createComponentConfig", + "params": { + "factoryPid": string, + "properties": [{ + "name": string, + "value": any + }] + } +} +---- + +and + +[source,json] +---- +{ + "method": "updateComponentConfig", + "params": { + "componentId": string, + "properties": [{ + "name": string, + "value": any + }] + } +} +---- + +The parameters for updating a component configuration are: + +- `componentId`: The unique ID of the Component +- `properties`: A set of properties with ‘name’ and ‘value + +_Example:_ The "Symmetric Balancing Schedule Controller" charges or discharges an ESS in order to reach a given power target setpoint at the grid connection point. Setpoints are given as a schedule in JSON format. Using an Update Component Config JSON-RPC request, an existing controller can be reconfigured with a new setpoint schedule: + +[source,json] +---- +{ + "jsonrpc": "2.0", + "id": UUID, + "method": "updateComponentConfig", + "params": { + "componentId": "ctrlBalancingSchedule0", + "properties": [ + { + "name": "schedule", + "value": [ + { + "startTimestamp": 1577836800, + "duration": 900, + "activePowerSetPoint": 0 + }, + { + "startTimestamp": 1577837700, + "duration": 900, + "activePowerSetPoint": -2000 + } + ] + } + ] + } +} +---- + +With this new configuration the algorithm will try to keep the grid connection point at `0 W` starting from timestamp `1577836800` (1st January 2020 00:00:00) for `900 seconds` (15 minutes) and at `-2000` (2000 W sell-to-grid power) starting from `1577837700` for another `900 seconds`. Outside of these two timeslots it sets no setpoint and allows a controller with lower priority to take over. + +### By editing/preseeding configuration files + +The OSGi Configuration Admin stores the configuration in plain text files on the filesystem. See the `felix.cm.dir` parameter in https://github.com/OpenEMS/openems/blob/develop/io.openems.edge.application/EdgeApp.bndrun[EdgeApp.bndrun] file as an example. + +This way can be used to conveniently preseed a configuration on an Edge device in productive deployment or for quick changes. Make sure to restart the application afterwards to apply changes. + +## Edge-Config + +.OpenEMS Edge Configuration Architecture +image::config-architecture.png[OpenEMS Edge Configuration Architecture] + +The architecture of OpenEMS Edge configuration is shown in the image above. It consists of + +Nature:: + +A Nature defines as set of characteristics and attributes. In OpenEMS Edge a Nature is a Java 'Interface', that defines required channels of an implementing OpenEMS Component. ++ +_Example:_ The Nature for a `Battery` defines Channels like `ChargeMaxVoltage`, `DischargeMaxVoltage` and `Soc` (state-of-charge) that need to be provided by every Battery implementation. + +Channel:: + +A Channel represents a single piece of information about a component; enriched with metadata like a description, unit of measure and more. ++ +_Example:_ The `ChargeMaxVoltage` channel of the Battery nature has a descriptive text "Maximal voltage for charging", is defined as type Integer with the unit Ampere. + +Factory:: + +A Factory is comparable to a 'Class' in object-oriented software development that is enriched with Java/OSGi metadata like a unique string identifier and defines a set of required configuration parameters. +A factory implements one or more Natures to indicate that it provides all channels defined by the Nature. +Additionally, a factory may define further channels that are specific to the individual implementation. ++ +_Example:_ The OpenEMS Edge "Factory" for BMW battery implements the `Battery` Nature. +Additionally, it declares channels like `AmbientTemperature` that are not available and required by every Battery implementation. + +Instance:: + +An Instance is comparable to an "Object", i.e. a runtime instantiation of a factory with defined configuration parameters. The Instance is then further referred to as an OpenEMS Component and uniquely identified by its Component-ID. + +OpenEMS Edge provides the specific configuration via its API in the form of a JSON definition referred to as **EdgeConfig**. The following shortened example shows its general structure: + +[source,json] +---- +{ + "components": { + "ess0": { + "alias": "Battery Energy Storage System", + "factoryId": "Ess.Generic.ManagedSymmetric", + "properties": { + "enabled": true, + "battery.id": "battery0", + "batteryInverter.id": "batteryInverter0" + }, + "channels": { + "ActivePower": { + "type": "INTEGER", + "accessMode": "RO", + "text": "Negative values for Charge; positive for Discharge", + "unit": "W" + } + } + } + }, + "factories": { + "Ess.Generic.ManagedSymmetric": { + "id": "Ess.Generic.ManagedSymmetric", + "name": "ESS Generic Managed Symmetric", + "description": "", + "natureIds": [ + "io.openems.edge.ess.api.SymmetricEss", + ], + "properties": [ + { + "id": "id", + "name": "Component-ID", + "description": "Unique ID of this Component", + "isRequired": true, + "defaultValue": "ess0" + } + ] + } + } +} +---- + +The EdgeConfig may be retrieved using the following JSON-RPC method: + +[source,json] +---- +{ + "jsonrpc": "2.0", + "id": "UUID", + "method": "getEdgeConfig", + "params": {} +} +---- -.OpenEMS UI Configuration -image::ui-config.png[OpenEMS UI Configuration] \ No newline at end of file +External services - like OpenEMS UI - can use the EdgeConfig to adapt to the actual configuration of the OpenEMS Edge. diff --git a/doc/modules/ROOT/pages/edge/controller.adoc b/doc/modules/ROOT/pages/edge/controller.adoc index 18ecbd0c8d0..3f6c203742a 100644 --- a/doc/modules/ROOT/pages/edge/controller.adoc +++ b/doc/modules/ROOT/pages/edge/controller.adoc @@ -11,7 +11,20 @@ A OpenEMS Edge Controller holds the actual business logic or the actual algorithm that controls hardware. The logic of each active Controller is executed regularly on every Cycle, i.e. once per second. -include::controller.adoc.d/_include.adoc[leveloffset=+0] +On each execution cycle, e.g. once every second, the EMS can send a control command, which allows two different ways of controlling the hardware: -// TODO -//=== Developing a Controller \ No newline at end of file +- **Control by setpoint** ++ +In “control by setpoint” mode, the EMS calculates setpoint commands and sends them to the hardware for immediate execution. ++ +_Example:_ A power smoothing algorithm uses the current and previous output power values of a photovoltaics system. By applying its configured maximum defined ramp rate it deduces that the energy storage systems needs to be discharged with 150 kW to compensate a suddenly reduced output power. It therefore sends a setpoint for "discharge with 150 kW" to the ESS for immediate execution. + +- **Control by parameterization** ++ +In “control by parameterization” mode, the EMS sends the configuration parameters for control algorithms embedded into the hardware for internal execution. ++ +_Example:_ In a Virtual Inertia Ancillary Services application, the EMS sends the configuration parameters for the control characteristics to the ESS. The ESS uses this parameter set for its internal, high-performance algorithm. + +The following Controllers are implemented in the OpenEMS standard codebase. The links point directly to the source code. + +include::controller.adoc.d/_include.adoc[leveloffset=+0] \ No newline at end of file diff --git a/doc/modules/ROOT/pages/edge/implement.adoc b/doc/modules/ROOT/pages/edge/implement.adoc index 05c4f5a4ca7..8e08c0c9182 100644 --- a/doc/modules/ROOT/pages/edge/implement.adoc +++ b/doc/modules/ROOT/pages/edge/implement.adoc @@ -65,7 +65,7 @@ image::eclipse-new-simulatedmeter-bundle.png[New simulated meter OpenEMS Modbus ==== You can see, that the Bundle is by default dependent on some core bundles -${buildpath}:: +$\{buildpath\}:: applies some defaults defined in `/cnf/build.bnd` io.openems.common:: OpenEMS commons @@ -129,7 +129,7 @@ OpenEMS Components can have several configuration parameters. They are defined a @AttributeDefinition(name = "Meter-Type", description = "Grid, Production (=default), Consumption") MeterType type() default MeterType.PRODUCTION; ---- -.. Set the `String webconsole_configurationFactory_nameHint()` default value to `"Meter Simulated [{id}]"` +.. Set the `String webconsole_configurationFactory_nameHint()` default value to `"Meter Simulated [\{id\}]"` . The content should now match the following code: + @@ -168,7 +168,7 @@ import io.openems.edge.meter.api.MeterType; @AttributeDefinition(name = "Modbus target filter", description = "This is auto-generated by 'Modbus-ID'.") String Modbus_target() default ""; // <9> - String webconsole_configurationFactory_nameHint() default "Meter Simulated [{id}]"; <10> + String webconsole_configurationFactory_nameHint() default "Meter Simulated [\{id\}]"; <10> } ---- diff --git a/doc/modules/ROOT/pages/gettingstarted.adoc b/doc/modules/ROOT/pages/gettingstarted.adoc index 00d4b474cd5..cea8fd20535 100644 --- a/doc/modules/ROOT/pages/gettingstarted.adoc +++ b/doc/modules/ROOT/pages/gettingstarted.adoc @@ -352,7 +352,7 @@ It is again noteworthy here, that: .UI via Backend image::ui-via-backend.png[UI via Backend] -# Next steps +## Next steps Now that you setup a complete development environment and have a working instance of OpenEMS Edge, OpenEMS Backend an OpenEMS UI, you can continue implementing your first device driver in OpenEMS. We provide a tutorial that explains the steps to implement an electric meter in OpenEMS Edge that is connected via Modbus/TCP. The meter itself is simulated using a small Modbus slave application, so no external hardware is required for this guide. → xref:edge/implement.adoc[Implementing a Device] diff --git a/doc/modules/ROOT/pages/introduction.adoc b/doc/modules/ROOT/pages/introduction.adoc index ed2f753268b..af3f0da9856 100644 --- a/doc/modules/ROOT/pages/introduction.adoc +++ b/doc/modules/ROOT/pages/introduction.adoc @@ -74,14 +74,14 @@ OpenEMS is funded by several federal and EU funding projects. If you are a devel * OpenEMS Edge * OpenEMS Backend -Copyright (C) 2016-2020 OpenEMS Association e.V., FENECON GmbH. +Copyright (C) 2016-2021 OpenEMS Association e.V., FENECON GmbH. This product includes software developed at FENECON GmbH: you can redistribute it and/or modify it under the terms of the https://github.com/OpenEMS/openems/blob/develop/LICENSE-EPL-2.0[Eclipse Public License version 2.0]. * OpenEMS UI -Copyright (C) 2016-2020 OpenEMS Association e.V., FENECON GmbH. +Copyright (C) 2016-2021 OpenEMS Association e.V., FENECON GmbH. This product includes software developed at FENECON GmbH: you can redistribute it and/or modify it under the terms of the https://github.com/OpenEMS/openems/blob/develop/LICENSE-AGPL-3.0[GNU Affero General Public License version 3]. \ No newline at end of file diff --git a/doc/modules/ROOT/pages/single_document.adoc b/doc/modules/ROOT/pages/single_document.adoc index 8852fdb9d11..a1a1c8a9311 100644 --- a/doc/modules/ROOT/pages/single_document.adoc +++ b/doc/modules/ROOT/pages/single_document.adoc @@ -1,7 +1,7 @@ = OpenEMS - Open Energy Management System ifndef::toc[] (c) 2020 OpenEMS Association e.V. -Version 2021.3.0 +Version 2021.4.0 :sectnums: :sectnumlevels: 4 :toc: @@ -30,16 +30,17 @@ include::edge/build.adoc[leveloffset=+2] include::edge/deploy.adoc[leveloffset=+2] == OpenEMS UI +include::ui/architecture.adoc[leveloffset=+2] include::ui/build.adoc[leveloffset=+2] == OpenEMS Backend include::backend/architecture.adoc[leveloffset=+2] -include::backend/configuration.adoc[leveloffset=+2] include::backend/backend-to-backend.adoc[leveloffset=+2] include::backend/build.adoc[leveloffset=+2] include::backend/deploy.adoc[leveloffset=+2] include::component-communication/index.adoc[leveloffset=+1] -include::simulation.adoc[leveloffset=+1] +include::simulation/realtime.adoc[leveloffset=+1] +include::simulation/ui-history.adoc[leveloffset=+1] include::documentation.adoc[leveloffset=+1] include::randd.adoc[leveloffset=+1] diff --git a/doc/modules/ROOT/pages/ui/architecture.adoc b/doc/modules/ROOT/pages/ui/architecture.adoc new file mode 100644 index 00000000000..f95264ac7d0 --- /dev/null +++ b/doc/modules/ROOT/pages/ui/architecture.adoc @@ -0,0 +1,26 @@ += UI Architecture +:sectnums: +:sectnumlevels: 4 +:toc: +:toclevels: 4 +:experimental: +:keywords: AsciiDoc +:source-highlighter: highlight.js +:icons: font +:imagesdir: ../../assets/images + +OpenEMS UI is the real-time user interface for web browsers and smartphones. + +.OpenEMS UI Live view +image::ui-live.png[OpenEMS UI Live view] + +== Adaptive User Interface + +The OpenEMS UI is the standard user interface for OpenEMS. +It uses the `EdgeConfig` (see Edge -> Configuration) to adapt its visualisation in accordance with the actual configuration. +The screenshot above visualizes the 'Live view' of OpenEMS UI. +It shows Storage System, Production and Grid because corresponding OpenEMS Components are listed in the EdgeConfig. + +== Configuration of OpenEMS Edge + +OpenEMS UI provides a way to instantiate an OpenEMS Component from a factory by providing configuration parameters. See Edge -> Configuration for details. diff --git a/io.openems.backend.application/BackendApp.bndrun b/io.openems.backend.application/BackendApp.bndrun index 028c0041dea..65864913ef6 100644 --- a/io.openems.backend.application/BackendApp.bndrun +++ b/io.openems.backend.application/BackendApp.bndrun @@ -37,7 +37,7 @@ com.google.gson;version='[2.8.5,2.8.6)',\ com.google.guava;version='[29.0.0,29.0.1)',\ com.google.guava.failureaccess;version='[1.0.1,1.0.2)',\ - com.zaxxer.HikariCP;version='[4.0.1,4.0.2)',\ + com.zaxxer.HikariCP;version='[4.0.2,4.0.3)',\ io.openems.backend.application;version=snapshot,\ io.openems.backend.b2brest;version=snapshot,\ io.openems.backend.b2bwebsocket;version=snapshot,\ @@ -72,9 +72,9 @@ org.apache.servicemix.bundles.ws-commons-util;version='[1.0.2,1.0.3)',\ org.apache.servicemix.bundles.xmlrpc-client;version='[3.1.3,3.1.4)',\ org.jsr-305;version='[3.0.2,3.0.3)',\ - org.ops4j.pax.logging.pax-logging-api;version='[2.0.6,2.0.7)',\ - org.ops4j.pax.logging.pax-logging-log4j1;version='[2.0.6,2.0.7)',\ + org.ops4j.pax.logging.pax-logging-api;version='[2.0.8,2.0.9)',\ + org.ops4j.pax.logging.pax-logging-log4j1;version='[2.0.8,2.0.9)',\ org.osgi.service.jdbc;version='[1.0.0,1.0.1)',\ org.osgi.util.function;version='[1.1.0,1.1.1)',\ org.osgi.util.promise;version='[1.1.1,1.1.2)',\ - org.postgresql.jdbc;version='[42.2.18,42.2.19)' \ No newline at end of file + org.postgresql.jdbc;version='[42.2.19,42.2.20)' \ No newline at end of file diff --git a/io.openems.common/resources/templates/controller/$srcDir$/$basePackageDir$/MyController.java b/io.openems.common/resources/templates/controller/$srcDir$/$basePackageDir$/MyController.java index f29d3859919..488adddddf4 100644 --- a/io.openems.common/resources/templates/controller/$srcDir$/$basePackageDir$/MyController.java +++ b/io.openems.common/resources/templates/controller/$srcDir$/$basePackageDir$/MyController.java @@ -13,15 +13,7 @@ import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.controller.api.Controller; -@Designate(ocd = Config.class, factory = true) -@Component(// - name = "Controller.$basePackageName$", // - immediate = true, // - configurationPolicy = ConfigurationPolicy.REQUIRE // -) -public class MyController extends AbstractOpenemsComponent implements Controller, OpenemsComponent { - - private Config config = null; +public interface MyController extends Controller, OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { ; @@ -38,26 +30,4 @@ public Doc doc() { } } - public MyController() { - super(// - OpenemsComponent.ChannelId.values(), // - Controller.ChannelId.values(), // - ChannelId.values() // - ); - } - - @Activate - void activate(ComponentContext context, Config config) { - super.activate(context, config.id(), config.alias(), config.enabled()); - this.config = config; - } - - @Deactivate - protected void deactivate() { - super.deactivate(); - } - - @Override - public void run() throws OpenemsNamedException { - } } diff --git a/io.openems.common/resources/templates/controller/$srcDir$/$basePackageDir$/MyControllerImpl.java b/io.openems.common/resources/templates/controller/$srcDir$/$basePackageDir$/MyControllerImpl.java new file mode 100644 index 00000000000..86502043727 --- /dev/null +++ b/io.openems.common/resources/templates/controller/$srcDir$/$basePackageDir$/MyControllerImpl.java @@ -0,0 +1,48 @@ +package $basePackageName$; + +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.metatype.annotations.Designate; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.controller.api.Controller; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Controller.$basePackageName$", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +public class MyControllerImpl extends AbstractOpenemsComponent implements MyController, Controller, OpenemsComponent { + + private Config config = null; + + public MyController() { + super(// + OpenemsComponent.ChannelId.values(), // + Controller.ChannelId.values(), // + MyController.ChannelId.values() // + ); + } + + @Activate + void activate(ComponentContext context, Config config) { + super.activate(context, config.id(), config.alias(), config.enabled()); + this.config = config; + } + + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + public void run() throws OpenemsNamedException { + } +} diff --git a/io.openems.common/resources/templates/controller/$testSrcDir$/$basePackageDir$/MyControllerTest.java b/io.openems.common/resources/templates/controller/$testSrcDir$/$basePackageDir$/MyControllerTest.java index 2964792764b..2205624e13f 100644 --- a/io.openems.common/resources/templates/controller/$testSrcDir$/$basePackageDir$/MyControllerTest.java +++ b/io.openems.common/resources/templates/controller/$testSrcDir$/$basePackageDir$/MyControllerTest.java @@ -10,8 +10,8 @@ public class MyControllerTest { private static final String CTRL_ID = "ctrl0"; @Test - private void test() throws Exception { - new ControllerTest(new MyController()) // + public void test() throws Exception { + new ControllerTest(new MyControllerImpl()) // .activate(MyConfig.create() // .setId(CTRL_ID) // .build()) diff --git a/io.openems.common/resources/templates/device-modbus/$srcDir$/$basePackageDir$/MyModbusDevice.java b/io.openems.common/resources/templates/device-modbus/$srcDir$/$basePackageDir$/MyModbusDevice.java index 3cf3abd59ac..4440a6c29a8 100644 --- a/io.openems.common/resources/templates/device-modbus/$srcDir$/$basePackageDir$/MyModbusDevice.java +++ b/io.openems.common/resources/templates/device-modbus/$srcDir$/$basePackageDir$/MyModbusDevice.java @@ -19,15 +19,7 @@ import io.openems.edge.common.channel.Doc; import io.openems.edge.common.component.OpenemsComponent; -@Designate(ocd = Config.class, factory = true) -@Component(// - name = "$basePackageName$", // - immediate = true, // - configurationPolicy = ConfigurationPolicy.REQUIRE // -) -public class MyModbusDevice extends AbstractOpenemsModbusComponent implements OpenemsComponent { - - private Config config = null; +public interface MyModbusDevice extends OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { ; @@ -44,43 +36,4 @@ public Doc doc() { } } - public MyModbusDevice() { - super(// - OpenemsComponent.ChannelId.values(), // - ChannelId.values() // - ); - } - - @Reference - protected ConfigurationAdmin cm; - - @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) - protected void setModbus(BridgeModbus modbus) { - super.setModbus(modbus); - } - - @Activate - void activate(ComponentContext context, Config config) throws OpenemsException { - if(super.activate(context, config.id(), config.alias(), config.enabled(), config.modbusUnitId(), this.cm, "Modbus", - config.modbus_id())) { - return; - } - this.config = config; - } - - @Deactivate - protected void deactivate() { - super.deactivate(); - } - - @Override - protected ModbusProtocol defineModbusProtocol() throws OpenemsException { - // TODO implement ModbusProtocol - return new ModbusProtocol(this); - } - - @Override - public String debugLog() { - return "Hello World"; - } } diff --git a/io.openems.common/resources/templates/device-modbus/$srcDir$/$basePackageDir$/MyModbusDeviceImpl.java b/io.openems.common/resources/templates/device-modbus/$srcDir$/$basePackageDir$/MyModbusDeviceImpl.java new file mode 100644 index 00000000000..52126c412fb --- /dev/null +++ b/io.openems.common/resources/templates/device-modbus/$srcDir$/$basePackageDir$/MyModbusDeviceImpl.java @@ -0,0 +1,71 @@ +package $basePackageName$; + +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.metatype.annotations.Designate; + +import io.openems.common.exceptions.OpenemsException; +import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; +import io.openems.edge.bridge.modbus.api.BridgeModbus; +import io.openems.edge.bridge.modbus.api.ModbusProtocol; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "$basePackageName$", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +public class MyModbusDeviceImpl extends AbstractOpenemsModbusComponent implements MyModbusDevice, OpenemsComponent { + + private Config config = null; + + public MyModbusDevice() { + super(// + OpenemsComponent.ChannelId.values(), // + MyModbusDevice.ChannelId.values() // + ); + } + + @Reference + protected ConfigurationAdmin cm; + + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) + protected void setModbus(BridgeModbus modbus) { + super.setModbus(modbus); + } + + @Activate + void activate(ComponentContext context, Config config) throws OpenemsException { + if(super.activate(context, config.id(), config.alias(), config.enabled(), config.modbusUnitId(), this.cm, "Modbus", + config.modbus_id())) { + return; + } + this.config = config; + } + + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + protected ModbusProtocol defineModbusProtocol() throws OpenemsException { + // TODO implement ModbusProtocol + return new ModbusProtocol(this); + } + + @Override + public String debugLog() { + return "Hello World"; + } +} diff --git a/io.openems.common/resources/templates/device-modbus/$testSrcDir$/$basePackageDir$/MyModbusDeviceTest.java b/io.openems.common/resources/templates/device-modbus/$testSrcDir$/$basePackageDir$/MyModbusDeviceTest.java index 703ff26c182..b429bb3bf95 100644 --- a/io.openems.common/resources/templates/device-modbus/$testSrcDir$/$basePackageDir$/MyModbusDeviceTest.java +++ b/io.openems.common/resources/templates/device-modbus/$testSrcDir$/$basePackageDir$/MyModbusDeviceTest.java @@ -12,8 +12,8 @@ public class MyModbusDeviceTest { private static final String MODBUS_ID = "modbus0"; @Test - private void test() throws Exception { - new ComponentTest(new MyModbusDevice()) // + public void test() throws Exception { + new ComponentTest(new MyModbusDeviceImpl()) // .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // .activate(MyConfig.create() // .setId(COMPONENT_ID) // diff --git a/io.openems.common/resources/templates/device/$srcDir$/$basePackageDir$/MyDevice.java b/io.openems.common/resources/templates/device/$srcDir$/$basePackageDir$/MyDevice.java index 36c4b46c72e..2c65574ca27 100644 --- a/io.openems.common/resources/templates/device/$srcDir$/$basePackageDir$/MyDevice.java +++ b/io.openems.common/resources/templates/device/$srcDir$/$basePackageDir$/MyDevice.java @@ -15,18 +15,7 @@ import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; -@Designate(ocd = Config.class, factory = true) -@Component(// - name = "$basePackageName$", // - immediate = true, // - configurationPolicy = ConfigurationPolicy.REQUIRE, // - property = { // - EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE // - } // -) -public class MyDevice extends AbstractOpenemsComponent implements OpenemsComponent, EventHandler { - - private Config config = null; +public interface MyDevice extends OpenemsComponent, EventHandler { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { ; @@ -42,39 +31,5 @@ public Doc doc() { return this.doc; } } - - public MyDevice() { - super(// - OpenemsComponent.ChannelId.values(), // - ChannelId.values() // - ); - } - - @Activate - void activate(ComponentContext context, Config config) { - super.activate(context, config.id(), config.alias(), config.enabled()); - this.config = config; - } - - @Deactivate - protected void deactivate() { - super.deactivate(); - } - - @Override - public void handleEvent(Event event) { - if (!this.isEnabled()) { - return; - } - switch (event.getTopic()) { - case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE: - // TODO: fill channels - break; - } - } - - @Override - public String debugLog() { - return "Hello World"; - } + } diff --git a/io.openems.common/resources/templates/device/$srcDir$/$basePackageDir$/MyDeviceImpl.java b/io.openems.common/resources/templates/device/$srcDir$/$basePackageDir$/MyDeviceImpl.java new file mode 100644 index 00000000000..5ec4a6fdd14 --- /dev/null +++ b/io.openems.common/resources/templates/device/$srcDir$/$basePackageDir$/MyDeviceImpl.java @@ -0,0 +1,65 @@ +package $basePackageName$; + +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.event.Event; +import org.osgi.service.event.EventConstants; +import org.osgi.service.event.EventHandler; +import org.osgi.service.metatype.annotations.Designate; + +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.event.EdgeEventConstants; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "$basePackageName$", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE, // + property = { // + EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE // + } // +) +public class MyDeviceImpl extends AbstractOpenemsComponent implements MyDevice, OpenemsComponent, EventHandler { + + private Config config = null; + + public MyDevice() { + super(// + OpenemsComponent.ChannelId.values(), // + MyDevice.ChannelId.values() // + ); + } + + @Activate + void activate(ComponentContext context, Config config) { + super.activate(context, config.id(), config.alias(), config.enabled()); + this.config = config; + } + + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + public void handleEvent(Event event) { + if (!this.isEnabled()) { + return; + } + switch (event.getTopic()) { + case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE: + // TODO: fill channels + break; + } + } + + @Override + public String debugLog() { + return "Hello World"; + } +} diff --git a/io.openems.common/resources/templates/device/$testSrcDir$/$basePackageDir$/MyDeviceTest.java b/io.openems.common/resources/templates/device/$testSrcDir$/$basePackageDir$/MyDeviceTest.java index 43a43d9ac82..d5cb74df7c9 100644 --- a/io.openems.common/resources/templates/device/$testSrcDir$/$basePackageDir$/MyDeviceTest.java +++ b/io.openems.common/resources/templates/device/$testSrcDir$/$basePackageDir$/MyDeviceTest.java @@ -10,8 +10,8 @@ public class MyDeviceTest { private static final String COMPONENT_ID = "component0"; @Test - private void test() throws Exception { - new ComponentTest(new MyDevice()) // + public void test() throws Exception { + new ComponentTest(new MyDeviceImpl()) // .activate(MyConfig.create() // .setId(COMPONENT_ID) // .build()) diff --git a/io.openems.common/src/io/openems/common/OpenemsConstants.java b/io.openems.common/src/io/openems/common/OpenemsConstants.java index bd15e2c8e99..8d3fcc55b0a 100644 --- a/io.openems.common/src/io/openems/common/OpenemsConstants.java +++ b/io.openems.common/src/io/openems/common/OpenemsConstants.java @@ -20,7 +20,7 @@ public class OpenemsConstants { * * This is usually the number of the sprint within the year */ - public final static short VERSION_MINOR = 3; + public final static short VERSION_MINOR = 4; /** * The patch version of OpenEMS. @@ -89,6 +89,7 @@ public class OpenemsConstants { */ public final static String CYCLE_ID = "_cycle"; public final static String COMPONENT_MANAGER_ID = "_componentManager"; + public final static String PREDICTOR_MANAGER_ID = "_predictorManager"; public final static String META_ID = "_meta"; public final static String SUM_ID = "_sum"; public final static String HOST_ID = "_host"; diff --git a/io.openems.common/src/io/openems/common/utils/StringUtils.java b/io.openems.common/src/io/openems/common/utils/StringUtils.java index 8315ea3d53a..497324145b1 100644 --- a/io.openems.common/src/io/openems/common/utils/StringUtils.java +++ b/io.openems.common/src/io/openems/common/utils/StringUtils.java @@ -28,10 +28,10 @@ public static String capitalizeFirstLetter(String s) { * Match two Strings, considering wildcards. * *
    - *
  • if {@link #equals(Object)} is true -> return 0 - *
  • if 'pattern' matches 'source' -> return value > 1; bigger values + *
  • if {@link #equals(Object)} is true -> return 0 + *
  • if 'pattern' matches 'source' -> return value > 1; bigger values * represent a better match - *
  • if both Strings do not match -> return -1 + *
  • if both Strings do not match -> return -1 *
* *

diff --git a/io.openems.common/src/io/openems/common/utils/UuidUtils.java b/io.openems.common/src/io/openems/common/utils/UuidUtils.java index ac9e02364fe..19d2e6e5611 100644 --- a/io.openems.common/src/io/openems/common/utils/UuidUtils.java +++ b/io.openems.common/src/io/openems/common/utils/UuidUtils.java @@ -7,7 +7,10 @@ public class UuidUtils { /** * Create a 'Nil' UUID: "00000000-0000-0000-0000-000000000000". * - * @see https://en.wikipedia.org/wiki/Universally_unique_identifier#Nil_UUID + *

+ * + * @see Wikipedia * * @return a Nil UUID */ diff --git a/io.openems.common/test/io/openems/common/types/ChannelAddressTest.java b/io.openems.common/test/io/openems/common/types/ChannelAddressTest.java new file mode 100644 index 00000000000..17bc159def9 --- /dev/null +++ b/io.openems.common/test/io/openems/common/types/ChannelAddressTest.java @@ -0,0 +1,31 @@ +package io.openems.common.types; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class ChannelAddressTest { + + @Test + public void test() { + ChannelAddress ess0ActivePower = new ChannelAddress("ess0", "ActivePower"); + ChannelAddress ess0ReactivePower = new ChannelAddress("ess0", "ReactivePower"); + ChannelAddress meter0ActivePower = new ChannelAddress("meter0", "ActivePower"); + ChannelAddress meter1ActivePower = new ChannelAddress("meter1", "ActivePower"); + ChannelAddress meter1ReactivePower = new ChannelAddress("meter1", "ReactivePower"); + ChannelAddress anyActivePower = new ChannelAddress("*", "ActivePower"); + ChannelAddress anyMeterActivePower = new ChannelAddress("meter*", "ActivePower"); + ChannelAddress anyPower = new ChannelAddress("*", "*Power"); + + assertEquals(0, ChannelAddress.match(ess0ActivePower, ess0ActivePower)); + assertEquals(-1, ChannelAddress.match(ess0ActivePower, ess0ReactivePower)); + assertEquals(Integer.MAX_VALUE / 2 + "*".length(), ChannelAddress.match(ess0ActivePower, anyActivePower)); + assertEquals(Integer.MAX_VALUE / 2 + "meter*".length(), + ChannelAddress.match(meter0ActivePower, anyMeterActivePower)); + assertEquals(Integer.MAX_VALUE / 2 + "meter*".length(), + ChannelAddress.match(meter1ActivePower, anyMeterActivePower)); + assertEquals("*".length() + "*Power".length(), ChannelAddress.match(meter1ActivePower, anyPower)); + assertEquals("*".length() + "*Power".length(), ChannelAddress.match(meter1ReactivePower, anyPower)); + } + +} diff --git a/io.openems.common/test/io/openems/common/utils/StringUtilsTest.java b/io.openems.common/test/io/openems/common/utils/StringUtilsTest.java index cf596422af2..9aae95441f9 100644 --- a/io.openems.common/test/io/openems/common/utils/StringUtilsTest.java +++ b/io.openems.common/test/io/openems/common/utils/StringUtilsTest.java @@ -1,6 +1,6 @@ package io.openems.common.utils; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; import org.junit.Test; @@ -8,18 +8,18 @@ import com.google.gson.JsonPrimitive; public class StringUtilsTest { - + @Test public void testToShortStringStringInt() { String test = "test to short string"; assertEquals("te...", StringUtils.toShortString(test, 5)); - + } @Test public void testToShortStringJsonObjectInt() { JsonObject j = new JsonObject(); - j.add("name", new JsonPrimitive("Testbert")); // {"name":"Testbert"} --> {"name":"T... + j.add("name", new JsonPrimitive("Testbert")); // {"name":"Testbert"} --> {"name":"T... assertEquals("{\"name\":\"T...", StringUtils.toShortString(j, 13)); } @@ -27,13 +27,26 @@ public void testToShortStringJsonObjectInt() { public void testCapitalizeFirstLetter() { assertEquals("Test", StringUtils.capitalizeFirstLetter("test")); assertEquals("TEST", StringUtils.capitalizeFirstLetter("TEST")); - assertEquals("1test", StringUtils.capitalizeFirstLetter("1test")); + assertEquals("1test", StringUtils.capitalizeFirstLetter("1test")); } - - @Test(expected=IndexOutOfBoundsException.class) + + @Test(expected = IndexOutOfBoundsException.class) public void testCapitalizeFirstLetterWithEmptyString() { assertEquals("", StringUtils.capitalizeFirstLetter("")); - + } + + @Test + public void testMatchWildcard() { + String activePower = "ActivePower"; + String anyPower = "*Power"; + String anyActive = "Active*"; + String any = "*"; + String foobar = "foobar"; + + assertEquals(anyPower.length(), StringUtils.matchWildcard(activePower, anyPower)); + assertEquals(anyActive.length(), StringUtils.matchWildcard(activePower, anyActive)); + assertEquals(1, StringUtils.matchWildcard(activePower, any)); + assertEquals(-1, StringUtils.matchWildcard(activePower, foobar)); } } diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index ddae1ca8880..6c6e73efb8f 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -150,7 +150,7 @@ com.google.gson;version='[2.8.5,2.8.6)',\ com.google.guava;version='[29.0.0,29.0.1)',\ com.google.guava.failureaccess;version='[1.0.1,1.0.2)',\ - com.sun.jna;version='[5.6.0,5.6.1)',\ + com.sun.jna;version='[5.7.0,5.7.1)',\ io.openems.common;version=snapshot,\ io.openems.edge.application;version=snapshot,\ io.openems.edge.battery.api;version=snapshot,\ @@ -300,8 +300,8 @@ org.jsr-305;version='[3.0.2,3.0.3)',\ org.openmuc.jmbus;version='[3.3.0,3.3.1)',\ org.openmuc.jrxtx;version='[1.0.1,1.0.2)',\ - org.ops4j.pax.logging.pax-logging-api;version='[2.0.6,2.0.7)',\ - org.ops4j.pax.logging.pax-logging-log4j1;version='[2.0.6,2.0.7)',\ + org.ops4j.pax.logging.pax-logging-api;version='[2.0.8,2.0.9)',\ + org.ops4j.pax.logging.pax-logging-log4j1;version='[2.0.8,2.0.9)',\ org.osgi.util.function;version='[1.1.0,1.1.1)',\ org.osgi.util.promise;version='[1.1.1,1.1.2)',\ rrd4j;version='[3.8.0,3.8.1)' \ No newline at end of file diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/api/Battery.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/api/Battery.java index 200ab163c69..c5d379cc6ec 100644 --- a/io.openems.edge.battery.api/src/io/openems/edge/battery/api/Battery.java +++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/api/Battery.java @@ -115,12 +115,9 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { *

  • Interface: Battery *
  • Type: Integer *
  • Unit: A - *
  • Usually positive, negative for force discharge mode; see - * {@link ChannelId#FORCE_DISCHARGE_ACTIVE} + *
  • Usually positive, negative for force discharge mode * */ - // TODO every Battery-Inverter implementation needs to be adjusted accordingly! - // Usually this register might be UINT16 and not accept negative values! CHARGE_MAX_CURRENT(Doc.of(OpenemsType.INTEGER) // .unit(Unit.AMPERE)), @@ -143,8 +140,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { *
  • Interface: Battery *
  • Type: Integer *
  • Unit: A - *
  • Usually positive, negative for force charge mode; see - * {@link ChannelId#FORCE_CHARGE_ACTIVE} + *
  • Usually positive, negative for force charge mode * */ DISCHARGE_MAX_CURRENT(Doc.of(OpenemsType.INTEGER) // @@ -199,6 +195,8 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { MAX_CELL_VOLTAGE(Doc.of(OpenemsType.INTEGER) // .unit(Unit.MILLIVOLT)), + // TODO FORCE_CHARGE_ACTIVE and FORCE_DISCHARGE_ACTIVE channels are + // deprecated/obsolete by BatteryProtection channels /** * Force charge active. * @@ -235,6 +233,7 @@ public Doc doc() { /** * Gets the ModbusSlaveNatureTable. + * * @param accessMode the {@link AccessMode} * @return ModbusSlaveNatureTable */ @@ -821,4 +820,5 @@ public default void _setForceDischargeActive(Boolean value) { public default void _setForceDischargeActive(boolean value) { this.getForceDischargeActiveChannel().setNextValue(value); } + } diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/BatteryProtection.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/BatteryProtection.java new file mode 100644 index 00000000000..b87af843273 --- /dev/null +++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/BatteryProtection.java @@ -0,0 +1,312 @@ +package io.openems.edge.battery.protection; + +import io.openems.common.channel.Unit; +import io.openems.common.types.OpenemsType; +import io.openems.edge.battery.api.Battery; +import io.openems.edge.battery.protection.currenthandler.ChargeMaxCurrentHandler; +import io.openems.edge.battery.protection.currenthandler.DischargeMaxCurrentHandler; +import io.openems.edge.battery.protection.force.AbstractForceChargeDischarge; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.ClockProvider; +import io.openems.edge.common.type.TypeUtils; + +/** + * This utility class provides algorithms to calculate maximum allowed charge + * and discharge currents for batteries. + * + *

    + * The logic uses: + * + *

      + *
    • Allowed Current Limit provided by Battery Management System + *
    • Voltage-to-Percent characteristics based on Min- and Max-Cell-Voltage + *
    • Temperature-to-Percent characteristics based on Min- and + * Max-Cell-Temperature + *
    • Linear max increase limit (e.g. 0.5 A per second) + *
    • Force Charge/Discharge mode (e.g. -1 A to enforce charge/discharge) + *
    + */ +public class BatteryProtection { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + /** + * Charge Current limit provided by the Battery/BMS. + * + *
      + *
    • Interface: BatteryProtection + *
    • Type: Integer + *
    • Unit: Ampere + *
    + */ + BP_CHARGE_BMS(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.AMPERE)), + /** + * Discharge Current limit provided by the Battery/BMS. + * + *
      + *
    • Interface: BatteryProtection + *
    • Type: Integer + *
    • Unit: Ampere + *
    + */ + BP_DISCHARGE_BMS(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.AMPERE)), + /** + * Charge Current limit derived from Min-Cell-Voltage. + * + *
      + *
    • Interface: BatteryProtection + *
    • Type: Integer + *
    • Unit: Ampere + *
    + */ + BP_CHARGE_MIN_VOLTAGE(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.AMPERE)), + /** + * Discharge Current limit derived from Min-Cell-Voltage. + * + *
      + *
    • Interface: BatteryProtection + *
    • Type: Integer + *
    • Unit: Ampere + *
    + */ + BP_DISCHARGE_MIN_VOLTAGE(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.AMPERE)), + /** + * Charge Current limit derived from Max-Cell-Voltage. + * + *
      + *
    • Interface: BatteryProtection + *
    • Type: Integer + *
    • Unit: Ampere + *
    + */ + BP_CHARGE_MAX_VOLTAGE(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.AMPERE)), + /** + * Discharge Current limit derived from Max-Cell-Voltage. + * + *
      + *
    • Interface: BatteryProtection + *
    • Type: Integer + *
    • Unit: Ampere + *
    + */ + BP_DISCHARGE_MAX_VOLTAGE(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.AMPERE)), + /** + * Charge Current limit derived from Min-Cell-Temperature. + * + *
      + *
    • Interface: BatteryProtection + *
    • Type: Integer + *
    • Unit: Ampere + *
    + */ + BP_CHARGE_MIN_TEMPERATURE(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.AMPERE)), + /** + * Discharge Current limit derived from Min-Cell-Temperature. + * + *
      + *
    • Interface: BatteryProtection + *
    • Type: Integer + *
    • Unit: Ampere + *
    + */ + BP_DISCHARGE_MIN_TEMPERATURE(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.AMPERE)), + /** + * Charge Current limit derived from Max-Cell-Temperature. + * + *
      + *
    • Interface: BatteryProtection + *
    • Type: Integer + *
    • Unit: Ampere + *
    + */ + BP_CHARGE_MAX_TEMPERATURE(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.AMPERE)), + /** + * Discharge Current limit derived from Max-Cell-Temperature. + * + *
      + *
    • Interface: BatteryProtection + *
    • Type: Integer + *
    • Unit: Ampere + *
    + */ + BP_DISCHARGE_MAX_TEMPERATURE(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.AMPERE)), + /** + * Charge Max-Increase Current limit. + * + *
      + *
    • Interface: BatteryProtection + *
    • Type: Integer + *
    • Unit: Ampere + *
    + */ + BP_CHARGE_INCREASE(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.AMPERE)), + /** + * Discharge Max-Increase Current limit. + * + *
      + *
    • Interface: BatteryProtection + *
    • Type: Integer + *
    • Unit: Ampere + *
    + */ + BP_DISCHARGE_INCREASE(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.AMPERE)), + /** + * Force-Discharge State. + * + *
      + *
    • Interface: BatteryProtection + *
    • Type: Integer + *
    • Unit: Ampere + *
    + */ + BP_FORCE_DISCHARGE(Doc.of(AbstractForceChargeDischarge.State.values())), // + /** + * Force-Charge State. + * + *
      + *
    • Interface: BatteryProtection + *
    • Type: Integer + *
    • Unit: Ampere + *
    + */ + BP_FORCE_CHARGE(Doc.of(AbstractForceChargeDischarge.State.values())) // + ; + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + + public static class Builder { + + private final Battery battery; + + private ChargeMaxCurrentHandler chargeMaxCurrentHandler; + private DischargeMaxCurrentHandler dischargeMaxCurrentHandler; + + protected Builder(Battery battery) { + this.battery = battery; + } + + /** + * Applies all values from a {@link BatteryProtectionDefinition}. + * + * @param def the {@link BatteryProtectionDefinition} + * @param clockProvider a {@link ClockProvider} + * @return a {@link Builder} + */ + public Builder applyBatteryProtectionDefinition(BatteryProtectionDefinition def, ClockProvider clockProvider) { + return this // + .setChargeMaxCurrentHandler( + ChargeMaxCurrentHandler.create(clockProvider, def.getInitialBmsMaxEverChargeCurrent()) // + .setVoltageToPercent(def.getChargeVoltageToPercent()) // + .setTemperatureToPercent(def.getChargeTemperatureToPercent()) // + .setMaxIncreasePerSecond(def.getMaxIncreaseAmperePerSecond()) // + .setForceDischarge(def.getForceDischargeParams()) // + .build()) // + .setDischargeMaxCurrentHandler( + DischargeMaxCurrentHandler.create(clockProvider, def.getInitialBmsMaxEverDischargeCurrent()) // + .setVoltageToPercent(def.getDischargeVoltageToPercent()) + .setTemperatureToPercent(def.getDischargeTemperatureToPercent()) // + .setMaxIncreasePerSecond(def.getMaxIncreaseAmperePerSecond()) // + .setForceCharge(def.getForceChargeParams()) // + .build()) // + ; + } + + /** + * Sets the {@link ChargeMaxCurrentHandler}. + * + * @param chargeMaxCurrentHandler the {@link ChargeMaxCurrentHandler} + * @return a {@link Builder} + */ + public Builder setChargeMaxCurrentHandler(ChargeMaxCurrentHandler chargeMaxCurrentHandler) { + this.chargeMaxCurrentHandler = chargeMaxCurrentHandler; + return this; + } + + /** + * Sets the {@link DischargeMaxCurrentHandler}. + * + * @param dischargeMaxCurrentHandler the {@link DischargeMaxCurrentHandler} + * @return a {@link Builder} + */ + public Builder setDischargeMaxCurrentHandler(DischargeMaxCurrentHandler dischargeMaxCurrentHandler) { + this.dischargeMaxCurrentHandler = dischargeMaxCurrentHandler; + return this; + } + + /** + * Builds the {@link BatteryProtection} instance. + * + * @return a {@link BatteryProtection} + */ + public BatteryProtection build() { + return new BatteryProtection(this.battery, this.chargeMaxCurrentHandler, this.dischargeMaxCurrentHandler); + } + } + + /** + * Create a {@link BatteryProtection} using builder pattern. + * + * @param battery the {@link Battery} + * @return a {@link Builder} + */ + public static Builder create(Battery battery) { + return new Builder(battery); + } + + private final Battery battery; + private final ChargeMaxCurrentHandler chargeMaxCurrentHandler; + private final DischargeMaxCurrentHandler dischargeMaxCurrentHandler; + + protected BatteryProtection(Battery battery, ChargeMaxCurrentHandler chargeMaxCurrentHandler, + DischargeMaxCurrentHandler dischargeMaxCurrentHandler) { + TypeUtils.assertNull("BatteryProtection algorithm is missing data", battery, chargeMaxCurrentHandler, + dischargeMaxCurrentHandler); + this.battery = battery; + this.chargeMaxCurrentHandler = chargeMaxCurrentHandler; + this.dischargeMaxCurrentHandler = dischargeMaxCurrentHandler; + } + + /** + * Apply the logic on the {@link Battery}. + * + *
      + *
    • Set CHARGE_MAX_CURRENT Channel + *
    • Set DISCHARGE_MAX_CURRENT Channel + *
    • Set FORCE_DISCHARGE_ACTIVE State-Channel if Charge-Max-Current < 0 + *
    • Set FORCE_CHARGE_ACTIVE State-Channel if Discharge-Max-Current < 0 + *
    • SET + *
    + */ + public void apply() { + // Use MaxCurrentHandlers to calculate max charge and discharge currents. + // These methods also write debug information to BatteryProtection-Channels, so + // it is feasible to always execute them, even if battery is not started. + int chargeMaxCurrent = this.chargeMaxCurrentHandler.calculateCurrentLimit(this.battery); + int dischargeMaxCurrent = this.dischargeMaxCurrentHandler.calculateCurrentLimit(this.battery); + + // Set max charge and discharge currents + this.battery._setChargeMaxCurrent(chargeMaxCurrent); + this.battery._setDischargeMaxCurrent(dischargeMaxCurrent); + } +} diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/BatteryProtectionDefinition.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/BatteryProtectionDefinition.java new file mode 100644 index 00000000000..fa85683afff --- /dev/null +++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/BatteryProtectionDefinition.java @@ -0,0 +1,95 @@ +package io.openems.edge.battery.protection; + +import io.openems.edge.battery.protection.force.ForceCharge; +import io.openems.edge.battery.protection.force.ForceDischarge; +import io.openems.edge.common.linecharacteristic.PolyLine; + +public interface BatteryProtectionDefinition { + + /** + * Defines the (estimated) maximum expected Charge current. + * + *

    + * This is used as a reference for percentage values in Voltage-To-Percent and + * Temperature-To-Percent definitions. If during runtime a higher value is + * provided, that one is taken from then on. + * + * @return the (estimated) maximum expected Charge current in [A] + */ + public int getInitialBmsMaxEverChargeCurrent(); + + /** + * Defines the (estimated) maximum expected Charge current. + * + *

    + * This is used as a reference for percentage values in Voltage-To-Percent and + * Temperature-To-Percent definitions. If during runtime a higher value is + * provided, that one is taken from then on. + * + * @return the (estimated) maximum expected Charge current in [A] + */ + public int getInitialBmsMaxEverDischargeCurrent(); + + /** + * Defines the Voltage-to-Percent limits for Charging. + * + *

    + * Voltage values are in [mV], Percentage in [0,1]. + * + * @return a {@link PolyLine} + */ + public PolyLine getChargeVoltageToPercent(); + + /** + * Defines the Voltage-to-Percent limits for Discharging. + * + *

    + * Voltage values are in [mV], Percentage in [0,1]. + * + * @return a {@link PolyLine} + */ + public PolyLine getDischargeVoltageToPercent(); + + /** + * Defines the Temperature-to-Percent limits for Charging. + * + *

    + * Temperature values are in [degC], Percentage in [0,1]. + * + * @return a {@link PolyLine} + */ + public PolyLine getChargeTemperatureToPercent(); + + /** + * Defines the Temperature-to-Percent limits for Discharging. + * + *

    + * Temperature values are in [degC], Percentage in [0,1]. + * + * @return a {@link PolyLine} + */ + public PolyLine getDischargeTemperatureToPercent(); + + /** + * Defines the parameters for Force-Discharge mode. + * + * @return the parameters + */ + public ForceDischarge.Params getForceDischargeParams(); + + /** + * Defines the parameters for Force-Charge mode. + * + * @return the ForceChargeParams + */ + public ForceCharge.Params getForceChargeParams(); + + /** + * Limits the maximum increase in [A] per second. Decrease is never limited for + * safety reasons. + * + * @return the limit or null + */ + public Double getMaxIncreaseAmperePerSecond(); + +} diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/AbstractMaxCurrentHandler.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/AbstractMaxCurrentHandler.java new file mode 100644 index 00000000000..2e5c248d21a --- /dev/null +++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/AbstractMaxCurrentHandler.java @@ -0,0 +1,418 @@ +package io.openems.edge.battery.protection.currenthandler; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.atomic.AtomicReference; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.battery.api.Battery; +import io.openems.edge.battery.protection.BatteryProtection.ChannelId; +import io.openems.edge.battery.protection.force.AbstractForceChargeDischarge; +import io.openems.edge.common.channel.IntegerReadChannel; +import io.openems.edge.common.component.ClockProvider; +import io.openems.edge.common.linecharacteristic.PolyLine; +import io.openems.edge.common.type.TypeUtils; + +public abstract class AbstractMaxCurrentHandler { + + public abstract static class Builder> { + protected final ClockProvider clockProvider; + protected final int initialBmsMaxEverCurrent; + + protected PolyLine voltageToPercent = PolyLine.empty(); + protected PolyLine temperatureToPercent = PolyLine.empty(); + protected Double maxIncreasePerSecond = null; + + /** + * Creates a {@link Builder} for {@link AbstractMaxCurrentHandler}. + * + * @param clockProvider a {@link ClockProvider}, mainly for JUnit + * tests + * @param initialBmsMaxEverCurrent the (estimated) maximum allowed current. This + * is used as a reference for percentage values. + * If during runtime a higher value is provided, + * that one is taken from then on. + */ + protected Builder(ClockProvider clockProvider, int initialBmsMaxEverCurrent) { + this.clockProvider = clockProvider; + this.initialBmsMaxEverCurrent = initialBmsMaxEverCurrent; + } + + /** + * Sets the Voltage-To-Percent characteristics. + * + * @param voltageToPercent the {@link PolyLine} + * @return a {@link Builder} + */ + public T setVoltageToPercent(PolyLine voltageToPercent) { + this.voltageToPercent = voltageToPercent; + return this.self(); + } + + /** + * Sets the Temperature-To-Percent characteristics. + * + * @param temperatureToPercent the {@link PolyLine} + * @return a {@link Builder} + */ + public T setTemperatureToPercent(PolyLine temperatureToPercent) { + this.temperatureToPercent = temperatureToPercent; + return this.self(); + } + + /** + * Sets the Max-Increase-Per-Second parameter in [A]. + * + * @param maxIncreasePerSecond value in [A] per Second. + * @return a {@link Builder} + */ + public T setMaxIncreasePerSecond(double maxIncreasePerSecond) { + this.maxIncreasePerSecond = maxIncreasePerSecond; + return this.self(); + } + + protected abstract T self(); + } + + protected final ClockProvider clockProvider; + protected final PolyLine voltageToPercent; + protected final PolyLine temperatureToPercent; + protected final AbstractForceChargeDischarge forceChargeDischarge; + + protected int bmsMaxEverCurrent; + + // used by 'getMaxIncreaseAmpereLimit()' + private final Double maxIncreasePerSecond; + protected Instant lastResultTimestamp = null; + protected Double lastCurrentLimit = null; + + protected AbstractMaxCurrentHandler(ClockProvider clockProvider, int initialBmsMaxEverCurrent, + PolyLine voltageToPercent, PolyLine temperatureToPercent, Double maxIncreasePerSecond, + AbstractForceChargeDischarge forceChargeDischarge) { + this.clockProvider = clockProvider; + this.bmsMaxEverCurrent = initialBmsMaxEverCurrent; + this.voltageToPercent = voltageToPercent; + this.temperatureToPercent = temperatureToPercent; + this.maxIncreasePerSecond = maxIncreasePerSecond; + this.forceChargeDischarge = forceChargeDischarge; + } + + /** + * Gets the ChannelId for Battery-Protection Limit originating from BMS. + * + *

      + *
    • {@link ChannelId#BP_CHARGE_BMS} + *
    • {@link ChannelId#BP_DISCHARGE_BMS} + *
    + * + * @return the {@link ChannelId} + */ + protected abstract ChannelId getBpBmsChannelId(); + + /** + * Gets the ChannelId for Battery-Protection Limit by Min-Cell-Voltage. + * + *
      + *
    • {@link ChannelId#BP_CHARGE_MIN_VOLTAGE} + *
    • {@link ChannelId#BP_DISCHARGE_MIN_VOLTAGE} + *
    + * + * @return the {@link ChannelId} + */ + protected abstract ChannelId getBpMinVoltageChannelId(); + + /** + * Gets the ChannelId for Battery-Protection Limit by Max-Cell-Voltage. + * + *
      + *
    • {@link ChannelId#BP_CHARGE_MAX_VOLTAGE} + *
    • {@link ChannelId#BP_DISCHARGE_MAX_VOLTAGE} + *
    + * + * @return the {@link ChannelId} + */ + protected abstract ChannelId getBpMaxVoltageChannelId(); + + /** + * Gets the ChannelId for Battery-Protection Limit by Min-Cell-Temperature. + * + *
      + *
    • {@link ChannelId#BP_CHARGE_MIN_TEMPERATURE} + *
    • {@link ChannelId#BP_DISCHARGE_MIN_TEMPERATURE} + *
    + * + * @return the {@link ChannelId} + */ + protected abstract ChannelId getBpMinTemperatureChannelId(); + + /** + * Gets the ChannelId for Battery-Protection Limit by Max-Cell-Temperature. + * + *
      + *
    • {@link ChannelId#BP_CHARGE_MAX_TEMPERATURE} + *
    • {@link ChannelId#BP_DISCHARGE_MAX_TEMPERATURE} + *
    + * + * @return the {@link ChannelId} + */ + protected abstract ChannelId getBpMaxTemperatureChannelId(); + + /** + * Gets the ChannelId for Battery-Protection Limit by Force Charge/Discharge + * Mode. + * + *
      + *
    • {@link ChannelId#BP_FORCE_CHARGE} + *
    • {@link ChannelId#BP_FORCE_DISCHARGE} + *
    + * + * @return the {@link ChannelId} + */ + protected abstract ChannelId getBpForceCurrentChannelId(); + + /** + * Gets the ChannelId for Battery-Protection Limit by Max-Increase-Ampere ramp. + * Mode. + * + *
      + *
    • {@link ChannelId#BP_FORCE_CHARGE} + *
    • {@link ChannelId#BP_FORCE_DISCHARGE} + *
    + * + * @return the {@link ChannelId} + */ + protected abstract ChannelId getBpMaxIncreaseAmpereChannelId(); + + /** + * Calculates the actual allowed current limit in [A] as minimum of:. + * + *
      + *
    • Is the battery started? (block any charge/discharge if not) + *
    • Allowed Current Limit provided by Battery Management System + *
    • Voltage-to-Percent characteristics for Min-Cell-Voltage + *
    • Voltage-to-Percent characteristics for Max-Cell-Voltage + *
    • Temperature-to-Percent characteristics for Min-Cell-Temperature + *
    • Temperature-to-Percent characteristics for Max-Cell-Temperature + *
    • Applied max increase limit (e.g. 0.5 A per second) + *
    • Force Charge/Discharge mode (e.g. -1 A to enforce charge/discharge) + *
    + * + * @param battery the {@link Battery} + * @return the actual allowed current limit, mathematically rounded to [A] + */ + public synchronized int calculateCurrentLimit(Battery battery) { + // Read input parameters from Battery + Integer minCellVoltage = battery.getMinCellVoltage().get(); + Integer maxCellVoltage = battery.getMaxCellVoltage().get(); + Integer minCellTemperature = battery.getMinCellTemperature().get(); + Integer maxCellTemperature = battery.getMaxCellTemperature().get(); + IntegerReadChannel bpBmsChannel = battery.channel(this.getBpBmsChannelId()); + Integer bpBms = bpBmsChannel.value().get(); + + // Update 'bmsMaxEverAllowedCurrent' + this.bmsMaxEverCurrent = TypeUtils.max(this.bmsMaxEverCurrent, bpBms); + + /* + * Get all limits + */ + // Calculate Ampere limit for Min-Cell-Voltage + final Double minCellVoltageLimit = this.getMinCellVoltageToPercentLimit(minCellVoltage); + // Calculate Ampere limit for Max-Cell-Voltage + final Double maxCellVoltageLimit = this.getMaxCellVoltageToPercentLimit(maxCellVoltage); + // Calculate Ampere limit for Min-Cell-Temperature + final Double minCellTemperatureLimit = this + .percentToAmpere(this.temperatureToPercent.getValue(minCellTemperature)); + // Calculate Ampere limit for Max-Cell-Temperature + final Double maxCellTemperatureLimit = this + .percentToAmpere(this.temperatureToPercent.getValue(maxCellTemperature)); + // Calculate Max Increase Ampere Limit + final Double maxIncreaseAmpereLimit = this.getMaxIncreaseAmpereLimit(); + // Calculate Force Current + final Double forceCurrent = this.getForceCurrent(minCellVoltage, maxCellVoltage); + + /* + * Store limits in Channels + */ + battery.channel(this.getBpMinVoltageChannelId()).setNextValue(minCellVoltageLimit); + battery.channel(this.getBpMaxVoltageChannelId()).setNextValue(maxCellVoltageLimit); + battery.channel(this.getBpMinTemperatureChannelId()).setNextValue(minCellTemperatureLimit); + battery.channel(this.getBpMaxTemperatureChannelId()).setNextValue(maxCellTemperatureLimit); + battery.channel(this.getBpMaxIncreaseAmpereChannelId()).setNextValue(maxIncreaseAmpereLimit); + battery.channel(this.getBpForceCurrentChannelId()).setNextValue(forceCurrent); + + // Get the minimum limit of all limits in Ampere + Double limit = TypeUtils.min(TypeUtils.toDouble(bpBms), minCellVoltageLimit, maxCellVoltageLimit, + minCellTemperatureLimit, maxCellTemperatureLimit, maxIncreaseAmpereLimit, forceCurrent); + + // Battery not started? Set '0' to block charge/discharge + if (!battery.isStarted()) { + limit = 0.; + } + + // No limit? Set '0' to block charge/discharge + if (limit == null) { + limit = 0.; + } + + this.lastCurrentLimit = limit; + + return (int) Math.round(limit); + } + + /** + * Calculates the current limit based on Min-/Max-Cell-Voltage according to the + * 'voltageToPercent' characteristics. + * + *

    + * If for the given 'cellVoltage' value 'voltageToPercent' defines a limitation + * (i.e. the given percentage is less than 100 %), that limitation stays active + * until a future 'cellVoltage' results in no limitation (i.e. percentage == 100 + * %). This is implemented to reduce fluctuations due to physical effects in the + * battery. + * + *

    + * This method internally uses the abstract + * {@link #getActiveCellVoltageToPercentLimit()} method to distinguish between + * active charge/discharge limitations. + * + * @param activeLimit the currently active limit + * @param cellVoltage the cell-voltage + * @return the Cell-Voltage-To-Percent Limit + */ + private synchronized Double getCellVoltageToPercentLimit(AtomicReference activeLimit, Integer cellVoltage) { + if (cellVoltage == null) { + return null; + } + Double percentage = this.voltageToPercent.getValue(cellVoltage); + if (percentage == null) { + return null; + } + + double thisCurrent = this.percentToAmpere(percentage); + final double result; + if (percentage > Math.nextDown(1)) { + // We are in the 'no limitation' zone of the PolyLine -> unset all limitations + result = thisCurrent; + activeLimit.set(null); + + } else { + // Current limit is active -> from now on only reduction of the limit is allowed + activeLimit.getAndUpdate(activeCurrent -> // + TypeUtils.min(activeCurrent, thisCurrent)); + result = activeLimit.get(); + } + return result; + } + + private final AtomicReference activeMinCellVoltageToPercentLimit = new AtomicReference<>(); + + protected Double getMinCellVoltageToPercentLimit(Integer minCellVoltage) { + return this.getCellVoltageToPercentLimit(this.activeMinCellVoltageToPercentLimit, minCellVoltage); + } + + private final AtomicReference activeMaxCellVoltageToPercentLimit = new AtomicReference<>(); + + protected Double getMaxCellVoltageToPercentLimit(Integer minCellVoltage) { + return this.getCellVoltageToPercentLimit(this.activeMaxCellVoltageToPercentLimit, minCellVoltage); + } + + /** + * Calculates the maximum increase limit in Ampere from the + * 'maxIncreasePerSecond' parameter. + * + *

    + * If maxIncreasePerSecond is 0.5, last limit was 10 A and 1 second passed, this + * method returns 10.5. + * + * @return the limit or null + */ + protected synchronized Double getMaxIncreaseAmpereLimit() { + if (this.maxIncreasePerSecond == null) { + return null; + } + Instant now = Instant.now(this.clockProvider.getClock()); + final Double result; + if (this.lastResultTimestamp != null && this.lastCurrentLimit != null) { + result = this.lastCurrentLimit + + (Duration.between(this.lastResultTimestamp, now).toMillis() * this.maxIncreasePerSecond) // + / 1000.; // convert [mA] to [A] + } else { + result = 0.; + } + this.lastResultTimestamp = now; + return result; + } + + /** + * Calculates the Ampere limit in Force Charge/Discharge mode. Returns: + * + *

      + *
    • -1 -> in force charge/discharge mode + *
    • 0 -> in block discharge/charge mode + *
    • null -> otherwise + *
    + * + * @param minCellVoltage the Min-Cell-Voltage, possibly null + * @param maxCellVoltage the Max-Cell-Voltage, possibly null + * @return the Current, possibly null + */ + protected Double getForceCurrent(Integer minCellVoltage, Integer maxCellVoltage) { + if (this.forceChargeDischarge == null) { + return null; + } + + final AbstractForceChargeDischarge.State state; + + // Apply State-Machine + if (minCellVoltage == null || maxCellVoltage == null) { + this.forceChargeDischarge.forceNextState(AbstractForceChargeDischarge.State.UNDEFINED); + state = AbstractForceChargeDischarge.State.UNDEFINED; + + } else { + try { + this.forceChargeDischarge.run( + new AbstractForceChargeDischarge.Context(this.clockProvider, minCellVoltage, maxCellVoltage)); + } catch (OpenemsNamedException e) { + e.printStackTrace(); + } + state = this.forceChargeDischarge.getCurrentState(); + } + + // Evaluate force charge/discharge current from current state + switch (state) { + case UNDEFINED: + case WAIT_FOR_FORCE_MODE: + return null; + case FORCE_MODE: + return -1.; + case BLOCK_MODE: + return 0.; + } + // will never happen + return null; + } + + /** + * Convert a Percent value to a concrete Ampere value in [A] by multiplying it + * with 'bmsMaxEverAllowedChargeCurrent'. + * + *
      + *
    • null % -> null + *
    • 0 % -> 0 + *
    • anything else -> calculate percent; at least '1 A'. + *
    + * + * @param percent the percent value in [0,1] + * @return the ampere value in [A] + */ + protected Double percentToAmpere(Double percent) { + if (percent == null) { + return null; + } else if (percent == 0.) { + return 0.; + } else { + return Math.max(1., this.bmsMaxEverCurrent * percent); + } + } + +} \ No newline at end of file diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/ChargeMaxCurrentHandler.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/ChargeMaxCurrentHandler.java new file mode 100644 index 00000000000..129a7c28c6e --- /dev/null +++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/ChargeMaxCurrentHandler.java @@ -0,0 +1,136 @@ +package io.openems.edge.battery.protection.currenthandler; + +import io.openems.edge.battery.protection.BatteryProtection; +import io.openems.edge.battery.protection.BatteryProtection.ChannelId; +import io.openems.edge.battery.protection.force.ForceDischarge; +import io.openems.edge.common.component.ClockProvider; +import io.openems.edge.common.linecharacteristic.PolyLine; + +public class ChargeMaxCurrentHandler extends AbstractMaxCurrentHandler { + + public static class Builder extends AbstractMaxCurrentHandler.Builder { + + private ForceDischarge.Params forceDischargeParams = null; + + /** + * Creates a {@link Builder} for {@link ChargeMaxCurrentHandler}. + * + * @param clockProvider a {@link ClockProvider}, mainly + * for JUnit tests + * @param initialBmsMaxEverAllowedChargeCurrent the (estimated) maximum allowed + * charge current. This is used as + * a reference for percentage + * values. If during runtime a + * higher value is provided, that + * one is taken from then on. + */ + protected Builder(ClockProvider clockProvider, int initialBmsMaxEverAllowedChargeCurrent) { + super(clockProvider, initialBmsMaxEverAllowedChargeCurrent); + } + + /** + * Configure 'Force Discharge' parameters. + * + * @param startDischargeAboveCellVoltage start force discharge if maxCellVoltage + * is above this value, e.g. 3660 + * @param dischargeAboveCellVoltage force discharge as long as + * maxCellVoltage is above this value, + * e.g. 3640 + * @param blockChargeAboveCellVoltage after 'force discharge', block charging + * as long as maxCellVoltage is above this + * value, e.g. 3450 + * @return {@link Builder} + */ + public Builder setForceDischarge(int startDischargeAboveCellVoltage, int dischargeAboveCellVoltage, + int blockChargeAboveCellVoltage) { + this.forceDischargeParams = new ForceDischarge.Params(startDischargeAboveCellVoltage, + dischargeAboveCellVoltage, blockChargeAboveCellVoltage); + return this; + } + + /** + * Sets the {@link ForceDischarge.Params} parameters. + * + * @param forceDischargeParams the {@link ForceDischarge.Params} + * @return a {@link Builder} + */ + public Builder setForceDischarge(ForceDischarge.Params forceDischargeParams) { + this.forceDischargeParams = forceDischargeParams; + return this; + } + + /** + * Builds the {@link ChargeMaxCurrentHandler} instance. + * + * @return a {@link ChargeMaxCurrentHandler} + */ + public ChargeMaxCurrentHandler build() { + return new ChargeMaxCurrentHandler(this.clockProvider, this.initialBmsMaxEverCurrent, this.voltageToPercent, + this.temperatureToPercent, this.maxIncreasePerSecond, this.forceDischargeParams); + } + + @Override + protected Builder self() { + return this; + } + } + + /** + * Create a {@link ChargeMaxCurrentHandler} builder. + * + * @param clockProvider a {@link ClockProvider} + * @param initialBmsMaxEverAllowedChargeCurrent the (estimated) maximum allowed + * charge current. This is used as + * a reference for percentage + * values. If during runtime a + * higher value is provided, that + * one is taken from then on. + * @return a {@link Builder} + */ + public static Builder create(ClockProvider clockProvider, int initialBmsMaxEverAllowedChargeCurrent) { + return new Builder(clockProvider, initialBmsMaxEverAllowedChargeCurrent); + } + + protected ChargeMaxCurrentHandler(ClockProvider clockProvider, int initialBmsMaxEverAllowedChargeCurrent, + PolyLine voltageToPercent, PolyLine temperatureToPercent, Double maxIncreasePerSecond, + ForceDischarge.Params forceDischargeParams) { + super(clockProvider, initialBmsMaxEverAllowedChargeCurrent, voltageToPercent, temperatureToPercent, + maxIncreasePerSecond, ForceDischarge.from(forceDischargeParams)); + } + + @Override + protected ChannelId getBpBmsChannelId() { + return BatteryProtection.ChannelId.BP_CHARGE_BMS; + } + + @Override + protected ChannelId getBpMinVoltageChannelId() { + return BatteryProtection.ChannelId.BP_CHARGE_MIN_VOLTAGE; + } + + @Override + protected ChannelId getBpMaxVoltageChannelId() { + return BatteryProtection.ChannelId.BP_CHARGE_MAX_VOLTAGE; + } + + @Override + protected ChannelId getBpMinTemperatureChannelId() { + return BatteryProtection.ChannelId.BP_CHARGE_MIN_TEMPERATURE; + } + + @Override + protected ChannelId getBpMaxTemperatureChannelId() { + return BatteryProtection.ChannelId.BP_CHARGE_MAX_TEMPERATURE; + } + + @Override + protected ChannelId getBpMaxIncreaseAmpereChannelId() { + return BatteryProtection.ChannelId.BP_CHARGE_INCREASE; + } + + @Override + protected ChannelId getBpForceCurrentChannelId() { + return BatteryProtection.ChannelId.BP_FORCE_DISCHARGE; + } + +} diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/DischargeMaxCurrentHandler.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/DischargeMaxCurrentHandler.java new file mode 100644 index 00000000000..8be74f934ea --- /dev/null +++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/DischargeMaxCurrentHandler.java @@ -0,0 +1,136 @@ +package io.openems.edge.battery.protection.currenthandler; + +import io.openems.edge.battery.protection.BatteryProtection; +import io.openems.edge.battery.protection.BatteryProtection.ChannelId; +import io.openems.edge.battery.protection.force.ForceCharge; +import io.openems.edge.common.component.ClockProvider; +import io.openems.edge.common.linecharacteristic.PolyLine; + +public class DischargeMaxCurrentHandler extends AbstractMaxCurrentHandler { + + public static class Builder extends AbstractMaxCurrentHandler.Builder { + + private ForceCharge.Params forceChargeParams = null; + + /** + * Creates a {@link Builder} for {@link DischargeMaxCurrentHandler}. + * + * @param clockProvider a {@link ClockProvider}, mainly for + * JUnit tests + * @param initialBmsMaxEverDischargeCurrent the (estimated) maximum allowed + * discharge current. This is used as a + * reference for percentage values. If + * during runtime a higher value is + * provided, that one is taken from + * then on. + */ + protected Builder(ClockProvider clockProvider, int initialBmsMaxEverDischargeCurrent) { + super(clockProvider, initialBmsMaxEverDischargeCurrent); + } + + /** + * Configure 'Force Charge' parameters. + * + * @param startChargeBelowCellVoltage start force charge if minCellVoltage is + * below this value, e.g. 2850 + * @param chargeBelowCellVoltage force charge as long as minCellVoltage + * is below this value, e.g. 2910 + * @param blockDischargeBelowCellVoltage after 'force charge', block discharging + * as long as minCellVoltage is below this + * value, e.g. 3000 + * @return {@link Builder} + */ + public Builder setForceCharge(int startChargeBelowCellVoltage, int chargeBelowCellVoltage, + int blockDischargeBelowCellVoltage) { + this.forceChargeParams = new ForceCharge.Params(startChargeBelowCellVoltage, chargeBelowCellVoltage, + blockDischargeBelowCellVoltage); + return this; + } + + /** + * Sets the {@link ForceCharge.Params} parameters. + * + * @param forceChargeParams the {@link ForceCharge.Params} + * @return a {@link Builder} + */ + public Builder setForceCharge(ForceCharge.Params forceChargeParams) { + this.forceChargeParams = forceChargeParams; + return this; + } + + /** + * Builds the {@link DischargeMaxCurrentHandler} instance. + * + * @return a {@link DischargeMaxCurrentHandler} + */ + public DischargeMaxCurrentHandler build() { + return new DischargeMaxCurrentHandler(this.clockProvider, this.initialBmsMaxEverCurrent, + this.voltageToPercent, this.temperatureToPercent, this.maxIncreasePerSecond, + ForceCharge.from(this.forceChargeParams)); + } + + @Override + protected Builder self() { + return this; + } + } + + /** + * Create a {@link DischargeMaxCurrentHandler} builder. + * + * @param clockProvider a {@link ClockProvider} + * @param initialBmsMaxEverDischargeCurrent the (estimated) maximum allowed + * discharge current. This is used as a + * reference for percentage values. If + * during runtime a higher value is + * provided, that one is taken from + * then on. + * @return a {@link Builder} + */ + public static Builder create(ClockProvider clockProvider, int initialBmsMaxEverDischargeCurrent) { + return new Builder(clockProvider, initialBmsMaxEverDischargeCurrent); + } + + protected DischargeMaxCurrentHandler(ClockProvider clockProvider, int initialBmsMaxEverDischargeCurrent, + PolyLine voltageToPercent, PolyLine temperatureToPercent, Double maxIncreasePerSecond, + ForceCharge forceCharge) { + super(clockProvider, initialBmsMaxEverDischargeCurrent, voltageToPercent, temperatureToPercent, + maxIncreasePerSecond, forceCharge); + } + + @Override + protected ChannelId getBpBmsChannelId() { + return BatteryProtection.ChannelId.BP_DISCHARGE_BMS; + } + + @Override + protected ChannelId getBpMinVoltageChannelId() { + return BatteryProtection.ChannelId.BP_DISCHARGE_MIN_VOLTAGE; + } + + @Override + protected ChannelId getBpMaxVoltageChannelId() { + return BatteryProtection.ChannelId.BP_DISCHARGE_MAX_VOLTAGE; + } + + @Override + protected ChannelId getBpMinTemperatureChannelId() { + return BatteryProtection.ChannelId.BP_DISCHARGE_MIN_TEMPERATURE; + } + + @Override + protected ChannelId getBpMaxTemperatureChannelId() { + return BatteryProtection.ChannelId.BP_DISCHARGE_MAX_TEMPERATURE; + } + + @Override + protected ChannelId getBpMaxIncreaseAmpereChannelId() { + return BatteryProtection.ChannelId.BP_DISCHARGE_INCREASE; + } + + @Override + protected ChannelId getBpForceCurrentChannelId() { + return BatteryProtection.ChannelId.BP_FORCE_CHARGE; + } + +} diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/package-info.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/package-info.java new file mode 100644 index 00000000000..741a4e32c90 --- /dev/null +++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/currenthandler/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.battery.protection.currenthandler; diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/force/AbstractForceChargeDischarge.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/force/AbstractForceChargeDischarge.java new file mode 100644 index 00000000000..607f9d7cb61 --- /dev/null +++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/force/AbstractForceChargeDischarge.java @@ -0,0 +1,132 @@ +package io.openems.edge.battery.protection.force; + +import java.time.Duration; +import java.time.Instant; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.OptionsEnum; +import io.openems.edge.common.component.ClockProvider; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.statemachine.AbstractContext; +import io.openems.edge.common.statemachine.AbstractStateMachine; +import io.openems.edge.common.statemachine.StateHandler; + +public abstract class AbstractForceChargeDischarge + extends AbstractStateMachine { + + protected static final int WAIT_FOR_FORCE_MODE_SECONDS = 60; + + public static class Context extends AbstractContext { + + private final ClockProvider clockProvider; + private final Integer minCellVoltage; + private final Integer maxCellVoltage; + + public Context(ClockProvider clockProvider, Integer minCellVoltage, Integer maxCellVoltage) { + super(); + this.clockProvider = clockProvider; + this.minCellVoltage = minCellVoltage; + this.maxCellVoltage = maxCellVoltage; + } + + protected Instant now() { + return Instant.now(this.clockProvider.getClock()); + } + } + + public enum State implements io.openems.edge.common.statemachine.State, OptionsEnum { + UNDEFINED(-1), // + + WAIT_FOR_FORCE_MODE(10), // + FORCE_MODE(11), // + BLOCK_MODE(12), // + ; + + private final int value; + + private State(int value) { + this.value = value; + } + + @Override + public int getValue() { + return this.value; + } + + @Override + public String getName() { + return this.name(); + } + + @Override + public OptionsEnum getUndefined() { + return UNDEFINED; + } + + @Override + public State[] getStates() { + return State.values(); + } + } + + public AbstractForceChargeDischarge() { + super(State.UNDEFINED); + } + + @Override + public StateHandler getStateHandler(State state) { + switch (state) { + case UNDEFINED: + return new StateHandler() { + @Override + protected State runAndGetNextState(Context context) throws OpenemsNamedException { + return AbstractForceChargeDischarge.this.handleUndefinedState(context.minCellVoltage, + context.maxCellVoltage); + } + }; + + case WAIT_FOR_FORCE_MODE: + return new StateHandler() { + private Instant enteredAt = Instant.MAX; + + protected void onEntry(Context context) throws OpenemsNamedException { + this.enteredAt = context.now(); + } + + @Override + protected State runAndGetNextState(Context context) throws OpenemsNamedException { + return AbstractForceChargeDischarge.this.handleWaitForForceModeState(context.minCellVoltage, + context.maxCellVoltage, Duration.between(this.enteredAt, context.now())); + } + }; + + case FORCE_MODE: + return new StateHandler() { + @Override + protected State runAndGetNextState(Context context) throws OpenemsNamedException { + return AbstractForceChargeDischarge.this.handleForceModeState(context.minCellVoltage, + context.maxCellVoltage); + } + }; + + case BLOCK_MODE: + return new StateHandler() { + @Override + protected State runAndGetNextState(Context context) throws OpenemsNamedException { + return AbstractForceChargeDischarge.this.handleBlockModeState(context.minCellVoltage, + context.maxCellVoltage); + } + }; + } + throw new IllegalArgumentException("Unknown State [" + state + "]"); + } + + protected abstract State handleUndefinedState(int minCellVoltage, int maxCellVoltage); + + protected abstract State handleWaitForForceModeState(int minCellVoltage, int maxCellVoltage, Duration sinceStart); + + protected abstract State handleForceModeState(int minCellVoltage, int maxCellVoltage); + + protected abstract State handleBlockModeState(int minCellVoltage, int maxCellVoltage); + +} \ No newline at end of file diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/force/ForceCharge.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/force/ForceCharge.java new file mode 100644 index 00000000000..e9b78c0968a --- /dev/null +++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/force/ForceCharge.java @@ -0,0 +1,88 @@ +package io.openems.edge.battery.protection.force; + +import java.time.Duration; + +public class ForceCharge extends AbstractForceChargeDischarge { + + /** + * Holds parameters for 'Force Charge' mode. + */ + public static class Params { + private final int startChargeBelowCellVoltage; + private final int chargeBelowCellVoltage; + private final int blockDischargeBelowCellVoltage; + + public Params(int startChargeBelowCellVoltage, int chargeBelowCellVoltage, int blockDischargeBelowCellVoltage) { + if (blockDischargeBelowCellVoltage < chargeBelowCellVoltage + || chargeBelowCellVoltage < startChargeBelowCellVoltage) { + throw new IllegalArgumentException( + "Make sure that startChargeBelowCellVoltage < chargeBelowCellVoltage < blockDischargeBelowCellVoltage."); + } + + this.startChargeBelowCellVoltage = startChargeBelowCellVoltage; + this.chargeBelowCellVoltage = chargeBelowCellVoltage; + this.blockDischargeBelowCellVoltage = blockDischargeBelowCellVoltage; + } + } + + private final Params params; + + /** + * Builds a {@link ForceCharge} instance from {@link ForceCharge.Params}. + * + * @param params the parameter object + * @return a {@link ForceCharge} instance + */ + public static ForceCharge from(Params params) { + if (params == null) { + return null; + } else { + return new ForceCharge(params); + } + } + + private ForceCharge(Params params) { + super(); + this.params = params; + } + + @Override + protected State handleUndefinedState(int minCellVoltage, int maxCellVoltage) { + if (minCellVoltage <= this.params.startChargeBelowCellVoltage) { + return State.WAIT_FOR_FORCE_MODE; + } else { + return State.UNDEFINED; + } + } + + @Override + protected State handleWaitForForceModeState(int minCellVoltage, int maxCellVoltage, Duration durationSinceStart) { + if (minCellVoltage > this.params.startChargeBelowCellVoltage) { + return State.UNDEFINED; + + } else if (durationSinceStart.getSeconds() < WAIT_FOR_FORCE_MODE_SECONDS) { + return State.WAIT_FOR_FORCE_MODE; + + } else { + return State.FORCE_MODE; + } + } + + @Override + protected State handleForceModeState(int minCellVoltage, int maxCellVoltage) { + if (minCellVoltage <= this.params.chargeBelowCellVoltage) { + return State.FORCE_MODE; + } else { + return State.BLOCK_MODE; + } + } + + @Override + protected State handleBlockModeState(int minCellVoltage, int maxCellVoltage) { + if (minCellVoltage <= this.params.blockDischargeBelowCellVoltage) { + return State.BLOCK_MODE; + } else { + return State.UNDEFINED; + } + } +} diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/force/ForceDischarge.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/force/ForceDischarge.java new file mode 100644 index 00000000000..3511bc0b9da --- /dev/null +++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/force/ForceDischarge.java @@ -0,0 +1,89 @@ +package io.openems.edge.battery.protection.force; + +import java.time.Duration; + +public class ForceDischarge extends AbstractForceChargeDischarge { + + /** + * Holds parameters for 'Force Discharge' mode. + */ + public static class Params { + private final int startDischargeAboveCellVoltage; + private final int dischargeAboveCellVoltage; + private final int blockChargeAboveCellVoltage; + + public Params(int startDischargeAboveCellVoltage, int dischargeAboveCellVoltage, + int blockChargeAboveCellVoltage) { + if (blockChargeAboveCellVoltage > dischargeAboveCellVoltage + || dischargeAboveCellVoltage > startDischargeAboveCellVoltage) { + throw new IllegalArgumentException( + "Make sure that startDischargeAboveCellVoltage > dischargeAboveCellVoltage > blockChargeAboveCellVoltage."); + } + + this.startDischargeAboveCellVoltage = startDischargeAboveCellVoltage; + this.dischargeAboveCellVoltage = dischargeAboveCellVoltage; + this.blockChargeAboveCellVoltage = blockChargeAboveCellVoltage; + } + } + + private final Params params; + + /** + * Builds a {@link ForceDischarge} instance from {@link ForceDischarge.Params}. + * + * @param params the parameter object + * @return a {@link ForceDischarge} instance + */ + public static ForceDischarge from(Params params) { + if (params == null) { + return null; + } else { + return new ForceDischarge(params); + } + } + + private ForceDischarge(Params params) { + super(); + this.params = params; + } + + @Override + protected State handleUndefinedState(int minCellVoltage, int maxCellVoltage) { + if (maxCellVoltage >= this.params.startDischargeAboveCellVoltage) { + return State.WAIT_FOR_FORCE_MODE; + } else { + return State.UNDEFINED; + } + } + + @Override + protected State handleWaitForForceModeState(int minCellVoltage, int maxCellVoltage, Duration durationSinceStart) { + if (maxCellVoltage < this.params.startDischargeAboveCellVoltage) { + return State.UNDEFINED; + + } else if (durationSinceStart.getSeconds() < WAIT_FOR_FORCE_MODE_SECONDS) { + return State.WAIT_FOR_FORCE_MODE; + + } else { + return State.FORCE_MODE; + } + } + + @Override + protected State handleForceModeState(int minCellVoltage, int maxCellVoltage) { + if (maxCellVoltage >= this.params.dischargeAboveCellVoltage) { + return State.FORCE_MODE; + } else { + return State.BLOCK_MODE; + } + } + + @Override + protected State handleBlockModeState(int minCellVoltage, int maxCellVoltage) { + if (maxCellVoltage >= this.params.blockChargeAboveCellVoltage) { + return State.BLOCK_MODE; + } else { + return State.UNDEFINED; + } + } +} diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/force/package-info.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/force/package-info.java new file mode 100644 index 00000000000..b0765402165 --- /dev/null +++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/force/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.battery.protection.force; diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/package-info.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/package-info.java new file mode 100644 index 00000000000..b02cd5adde2 --- /dev/null +++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.battery.protection; diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/readme.adoc b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/readme.adoc new file mode 100644 index 00000000000..bec7ecaa00f --- /dev/null +++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/protection/readme.adoc @@ -0,0 +1,83 @@ += Battery-Protection + +The "Battery-Protection" implementation serves as an addition to low-level battery management systems (BMS). +It allows a fine grained definition of battery protection parameters and handles logics that are shared between different BMS implementations. + +== Implementation details + +The Battery-Protection utilities allow the definition of the following parameters. They are used to evaluate the allowed Maximum Charge Current and Maximum Discharge Current. Consequently the Battery-Protection internally sets the respective Channels `Battery.ChargeMaxCurrent` and `Battery.DischargeMaxCurrent`. Be sure to not directly map the BMS registers to these Channels if you use the Battery-Protection utilities. + +== Input + +The Battery-Protection utilities require a number of specific Channels provided in `BatteryProtection.ChannelId`. + +The Channels `BP_CHARGE_BMS` and `BP_DISCHARGE_BMS` are meant to be mapped to the data originally provided by the BMS. The Unit is Ampere. + +== Output + +The Battery-Protection utilities evaluate the allowed Maximum Charge Current and Maximum Discharge Current. Consequently the Battery-Protection internally sets the respective Channels `Battery.ChargeMaxCurrent` and `Battery.DischargeMaxCurrent`. + +WARNING: Be sure to not directly map the BMS registers to these Channels if you use the Battery-Protection utilities. + +== Parameters + +The easiest way to provide all required parameters is via a class that implements the `BatteryProtectionDefintion` interface. See that interface for details on the parameters. + +== Debugging + +The utilities write data to the Channels provided in `BatteryProtection.ChannelId`. These Channels are useful to debug the behavior of the battery protection algorithm, i.e. finding out, why the Maximum Charge Current or Maximum Discharge Current are limited in a specific case. + +== Using the Battery-Protection + +To use the Battery-Protection utilities in your BMS component: + +. Implement the `BatteryProtection.ChannelId`s ++ +[source,java] +---- +public BatteryImpl() { + super(// + OpenemsComponent.ChannelId.values(), // + Battery.ChannelId.values(), // + StartStoppable.ChannelId.values(), // + BatteryProtection.ChannelId.values() // <-- + ); +} +---- + +. Hold a local instance of `BatteryProtection` ++ +[source,java] +---- +private BatteryProtection batteryProtection = null; +---- + +. Initialize the `batteryProtection` instance in your `activate()` method ++ +[source,java] +---- +this.batteryProtection = BatteryProtection.create(this) // + .applyBatteryProtectionDefinition(new MyBatteryProtectionDefinition(), this.componentManager) // + .build(); +---- ++ +The easiest way is via a class that implements the `BatteryProtectionDefintion` interface. This will guide you through all the required battery protection parameters. Alternatively there is also an initialization that follows the Builder pattern. See the JUnit tests for examples. + +. Call the logic ++ +Call the BatteryProtection logic in every cycle, e.g. in an Eventhandler: ++ +[source,java] +---- +@Override +public void handleEvent(Event event) { + if (!this.isEnabled()) { + return; + } + switch (event.getTopic()) { + case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE: + this.batteryProtection.apply(); + break; + } +} +---- \ No newline at end of file diff --git a/io.openems.edge.battery.api/src/io/openems/edge/battery/test/DummyBattery.java b/io.openems.edge.battery.api/src/io/openems/edge/battery/test/DummyBattery.java index 9510767ce47..8ef66fde436 100644 --- a/io.openems.edge.battery.api/src/io/openems/edge/battery/test/DummyBattery.java +++ b/io.openems.edge.battery.api/src/io/openems/edge/battery/test/DummyBattery.java @@ -15,10 +15,15 @@ public class DummyBattery extends AbstractOpenemsComponent implements Battery, OpenemsComponent, StartStoppable { public DummyBattery(String id) { + this(id, new io.openems.edge.common.channel.ChannelId[0]); + } + + public DummyBattery(String id, io.openems.edge.common.channel.ChannelId[] additionalChannelIds) { super(// OpenemsComponent.ChannelId.values(), // StartStoppable.ChannelId.values(), // - Battery.ChannelId.values() // + Battery.ChannelId.values(), // + additionalChannelIds // ); for (Channel channel : this.channels()) { channel.nextProcessImage(); @@ -32,9 +37,10 @@ public void setStartStop(StartStop value) throws OpenemsNamedException { } /** - * withCapacity. - * @param value int - * @return DummyBattery + * Sets and applies the {@link Battery.ChannelId#CAPACITY}. + * + * @param value the Capacity in [Wh] + * @return myself */ public DummyBattery withCapacity(int value) { this._setCapacity(value); @@ -43,9 +49,10 @@ public DummyBattery withCapacity(int value) { } /** - * withVoltage. - * @param value int - * @return DummyBattery + * Sets and applies the {@link Battery.ChannelId#VOLTAGE}. + * + * @param value the Capacity in [V] + * @return myself */ public DummyBattery withVoltage(int value) { this._setVoltage(value); @@ -54,9 +61,10 @@ public DummyBattery withVoltage(int value) { } /** - * withDischargeMaxCurrent. - * @param value int - * @return DummyBattery + * Sets and applies the {@link Battery.ChannelId#DISCHARGE_MAX_CURRENT}. + * + * @param value the Discharge Max Current in [A] + * @return myself */ public DummyBattery withDischargeMaxCurrent(int value) { this._setDischargeMaxCurrent(value); @@ -65,9 +73,10 @@ public DummyBattery withDischargeMaxCurrent(int value) { } /** - * withChargeMaxCurrent. - * @param value int - * @return DummyBattery + * Sets and applies the {@link Battery.ChannelId#CHARGE_MAX_CURRENT}. + * + * @param value the Charge Max Current in [A] + * @return myself */ public DummyBattery withChargeMaxCurrent(int value) { this._setChargeMaxCurrent(value); @@ -76,9 +85,10 @@ public DummyBattery withChargeMaxCurrent(int value) { } /** - * withMinCellVoltage. - * @param value int - * @return DummyBattery + * Sets and applies the {@link Battery.ChannelId#MIN_CELL_VOLTAGE}. + * + * @param value the Min-Cell-Voltage in [mV] + * @return myself */ public DummyBattery withMinCellVoltage(int value) { this._setMinCellVoltage(value); @@ -87,9 +97,10 @@ public DummyBattery withMinCellVoltage(int value) { } /** - * withMaxCellVoltage. - * @param value int - * @return DummyBattery + * Sets and applies the {@link Battery.ChannelId#MAX_CELL_VOLTAGE}. + * + * @param value the Max-Cell-Voltage in [mV] + * @return myself */ public DummyBattery withMaxCellVoltage(int value) { this._setMaxCellVoltage(value); @@ -97,4 +108,28 @@ public DummyBattery withMaxCellVoltage(int value) { return this; } + /** + * Sets and applies the {@link Battery.ChannelId#MIN_CELL_TEMPERATURE}. + * + * @param value the Min-Cell-Temperature in [degC] + * @return myself + */ + public DummyBattery withMinCellTemperature(int value) { + this._setMinCellTemperature(value); + this.getMinCellTemperatureChannel().nextProcessImage(); + return this; + } + + /** + * Sets and applies the {@link Battery.ChannelId#MAX_CELL_TEMPERATURE}. + * + * @param value the Max-Cell-Temperature in [degC] + * @return myself + */ + public DummyBattery withMaxCellTemperature(int value) { + this._setMaxCellTemperature(value); + this.getMaxCellTemperatureChannel().nextProcessImage(); + return this; + } + } diff --git a/io.openems.edge.battery.api/test/io/openems/edge/battery/api/DummyBattery.java b/io.openems.edge.battery.api/test/io/openems/edge/battery/api/DummyBattery.java index 724a2ed4a85..952b486dfb9 100644 --- a/io.openems.edge.battery.api/test/io/openems/edge/battery/api/DummyBattery.java +++ b/io.openems.edge.battery.api/test/io/openems/edge/battery/api/DummyBattery.java @@ -7,6 +7,7 @@ import io.openems.edge.common.startstop.StartStop; import io.openems.edge.common.startstop.StartStoppable; +// TODO merge with io.openems.edge.battery.test.DummyBattery public class DummyBattery extends AbstractOpenemsComponent implements Battery, StartStoppable { public static final int DEFAULT_SOC = 50; @@ -41,7 +42,7 @@ protected DummyBattery(// void setMinimalCellVoltage(int minimalCellVoltage) { this._setMinCellVoltage(minimalCellVoltage); this.getMinCellVoltageChannel().nextProcessImage(); - } + } void setMinimalCellVoltageToUndefined() { this._setMinCellVoltage(null); @@ -87,7 +88,7 @@ void setSocToUndefined() { this._setSoc(null); this.getSocChannel().nextProcessImage(); } - + void setVoltage(int voltage) { this._setVoltage(voltage); this.getVoltageChannel().nextProcessImage(); @@ -97,7 +98,7 @@ void setVoltageToUndefined() { this._setVoltage(null); this.getVoltageChannel().nextProcessImage(); } - + void setCapacity(int capacity) { this._setCapacity(capacity); this.getCapacityChannel().nextProcessImage(); @@ -107,7 +108,7 @@ void setCapacityToUndefined() { this._setCapacity(null); this.getCapacityChannel().nextProcessImage(); } - + void setForceDischargeActive(boolean active) { this._setForceDischargeActive(active); this.getForceDischargeActiveChannel().nextProcessImage(); @@ -117,7 +118,7 @@ void setForceDischargeActiveToUndefined() { this._setForceDischargeActive(null); this.getForceDischargeActiveChannel().nextProcessImage(); } - + void setForceChargeActive(boolean active) { this._setForceChargeActive(active); this.getForceChargeActiveChannel().nextProcessImage(); @@ -127,7 +128,7 @@ void setForceChargeActiveToUndefined() { this._setForceChargeActive(null); this.getForceChargeActiveChannel().nextProcessImage(); } - + void setChargeMaxCurrent(int value) { this._setChargeMaxCurrent(value); this.getChargeMaxCurrentChannel().nextProcessImage(); @@ -137,7 +138,7 @@ void setChargeMaxCurrentToUndefined() { this._setChargeMaxCurrent(null); this.getChargeMaxCurrentChannel().nextProcessImage(); } - + void setDischargeMaxCurrent(int value) { this._setDischargeMaxCurrent(value); this.getDischargeMaxCurrentChannel().nextProcessImage(); @@ -147,7 +148,7 @@ void setDischargeMaxCurrentToUndefined() { this._setDischargeMaxCurrent(null); this.getDischargeMaxCurrentChannel().nextProcessImage(); } - + @Override public void setStartStop(StartStop value) throws OpenemsNamedException { // TODO start stop is not implemented diff --git a/io.openems.edge.battery.api/test/io/openems/edge/battery/api/DummyCellCharacteristic.java b/io.openems.edge.battery.api/test/io/openems/edge/battery/api/DummyCellCharacteristic.java index df64ec7d9a5..51bc4507f46 100644 --- a/io.openems.edge.battery.api/test/io/openems/edge/battery/api/DummyCellCharacteristic.java +++ b/io.openems.edge.battery.api/test/io/openems/edge/battery/api/DummyCellCharacteristic.java @@ -1,7 +1,7 @@ package io.openems.edge.battery.api; public class DummyCellCharacteristic implements CellCharacteristic { - + public static final int FINAL_CELL_CHARGE_VOLTAGE_MV = 3_650; public static final int FINAL_CELL_DISCHARGE_VOLTAGE_MV = 2_900; public static final int FORCE_CHARGE_CELL_VOLTAGE_MV = 2_800; diff --git a/io.openems.edge.battery.api/test/io/openems/edge/battery/api/SetAllowedCurrentsTest.java b/io.openems.edge.battery.api/test/io/openems/edge/battery/api/SetAllowedCurrentsTest.java index 13507c1ce1a..d6c38051c10 100644 --- a/io.openems.edge.battery.api/test/io/openems/edge/battery/api/SetAllowedCurrentsTest.java +++ b/io.openems.edge.battery.api/test/io/openems/edge/battery/api/SetAllowedCurrentsTest.java @@ -10,114 +10,123 @@ public class SetAllowedCurrentsTest { private DummyBattery battery; - private DummyCellCharacteristic cellCharacteristic; + private DummyCellCharacteristic cellCharacteristic; private Settings settings; - + private static int MAX_INCREASE_MILLI_AMPERE = 300; private static double MIN_CURRENT_AMPERE = 1; private static int TOLERANCE_MILLI_VOLT = 10; private static double POWER_FACTOR = 0.02; - + @Before public void setUp() throws Exception { this.battery = new DummyBattery(); this.cellCharacteristic = new DummyCellCharacteristic(); - this.settings = new SettingsImpl(TOLERANCE_MILLI_VOLT, MIN_CURRENT_AMPERE, POWER_FACTOR, MAX_INCREASE_MILLI_AMPERE); + this.settings = new SettingsImpl(TOLERANCE_MILLI_VOLT, MIN_CURRENT_AMPERE, POWER_FACTOR, + MAX_INCREASE_MILLI_AMPERE); } @Test - public void testBatteryIsChargedUntilFinalDischargeIsReached() { + public void testBatteryIsChargedUntilFinalDischargeIsReached() { // Battery has to be charged int maxDischargeCurrentFromBms = 0; int maxChargeCurrentFromBms = DummyBattery.DEFAULT_MAX_CHARGE_CURRENT; this.battery.setMinimalCellVoltage(DummyCellCharacteristic.FORCE_CHARGE_CELL_VOLTAGE_MV); - SetAllowedCurrents.setMaxAllowedCurrents(this.battery, this.cellCharacteristic, this.settings, maxChargeCurrentFromBms, maxDischargeCurrentFromBms); - + SetAllowedCurrents.setMaxAllowedCurrents(this.battery, this.cellCharacteristic, this.settings, + maxChargeCurrentFromBms, maxDischargeCurrentFromBms); + this.battery.getChargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceDischargeActiveChannel().nextProcessImage(); this.battery.getDischargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceChargeActiveChannel().nextProcessImage(); - + int expectedMaxChargeCurrent = maxChargeCurrentFromBms; int actualMaxChargeCurrent = this.battery.getChargeMaxCurrent().get(); assertEquals(expectedMaxChargeCurrent, actualMaxChargeCurrent); - - int expectedMaxDischargeCurrent = - (int) Math.max(MIN_CURRENT_AMPERE, this.battery.getCapacity().get() * POWER_FACTOR / this.battery.getVoltage().get()); + + int expectedMaxDischargeCurrent = -(int) Math.max(MIN_CURRENT_AMPERE, + this.battery.getCapacity().get() * POWER_FACTOR / this.battery.getVoltage().get()); int actualMaxDischargeCurrent = this.battery.getDischargeMaxCurrent().get(); assertEquals(expectedMaxDischargeCurrent, actualMaxDischargeCurrent); - + boolean expectedChargeForce = true; boolean actualChargeForce = this.battery.getForceChargeActive().get(); assertEquals(expectedChargeForce, actualChargeForce); - + boolean expectedDischargeForce = false; boolean actualdischargeForce = this.battery.getForceDischargeActive().get(); assertEquals(expectedDischargeForce, actualdischargeForce); - - // Min Voltage has risen above force level, but is still under final discharge level minus tolerance - this.battery.setMinimalCellVoltage(DummyCellCharacteristic.FINAL_CELL_DISCHARGE_VOLTAGE_MV - this.settings.getToleranceMilliVolt() - 1); - SetAllowedCurrents.setMaxAllowedCurrents(this.battery, this.cellCharacteristic, this.settings, maxChargeCurrentFromBms, maxDischargeCurrentFromBms); - + + // Min Voltage has risen above force level, but is still under final discharge + // level minus tolerance + this.battery.setMinimalCellVoltage( + DummyCellCharacteristic.FINAL_CELL_DISCHARGE_VOLTAGE_MV - this.settings.getToleranceMilliVolt() - 1); + SetAllowedCurrents.setMaxAllowedCurrents(this.battery, this.cellCharacteristic, this.settings, + maxChargeCurrentFromBms, maxDischargeCurrentFromBms); + this.battery.getChargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceDischargeActiveChannel().nextProcessImage(); this.battery.getDischargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceChargeActiveChannel().nextProcessImage(); - + expectedMaxChargeCurrent = maxChargeCurrentFromBms; actualMaxChargeCurrent = this.battery.getChargeMaxCurrent().get(); assertEquals(expectedMaxChargeCurrent, actualMaxChargeCurrent); - - expectedMaxDischargeCurrent = - (int) Math.max(MIN_CURRENT_AMPERE, this.battery.getCapacity().get() * POWER_FACTOR / this.battery.getVoltage().get()); + + expectedMaxDischargeCurrent = -(int) Math.max(MIN_CURRENT_AMPERE, + this.battery.getCapacity().get() * POWER_FACTOR / this.battery.getVoltage().get()); actualMaxDischargeCurrent = this.battery.getDischargeMaxCurrent().get(); assertEquals(expectedMaxDischargeCurrent, actualMaxDischargeCurrent); - + expectedChargeForce = true; actualChargeForce = this.battery.getForceChargeActive().get(); assertEquals(expectedChargeForce, actualChargeForce); - + expectedDischargeForce = false; actualdischargeForce = this.battery.getForceDischargeActive().get(); assertEquals(expectedDischargeForce, actualdischargeForce); - - // Min Voltage has risen above final discharge level + + // Min Voltage has risen above final discharge level this.battery.setMinimalCellVoltage(DummyCellCharacteristic.FINAL_CELL_DISCHARGE_VOLTAGE_MV + 1); - SetAllowedCurrents.setMaxAllowedCurrents(this.battery, this.cellCharacteristic, this.settings, maxChargeCurrentFromBms, maxDischargeCurrentFromBms); - + SetAllowedCurrents.setMaxAllowedCurrents(this.battery, this.cellCharacteristic, this.settings, + maxChargeCurrentFromBms, maxDischargeCurrentFromBms); + this.battery.getChargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceDischargeActiveChannel().nextProcessImage(); this.battery.getDischargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceChargeActiveChannel().nextProcessImage(); - + expectedMaxChargeCurrent = maxChargeCurrentFromBms; actualMaxChargeCurrent = this.battery.getChargeMaxCurrent().get(); assertEquals(expectedMaxChargeCurrent, actualMaxChargeCurrent); - + expectedMaxDischargeCurrent = maxDischargeCurrentFromBms; actualMaxDischargeCurrent = this.battery.getDischargeMaxCurrent().get(); assertEquals(expectedMaxDischargeCurrent, actualMaxDischargeCurrent); - + expectedChargeForce = false; actualChargeForce = this.battery.getForceChargeActive().get(); assertEquals(expectedChargeForce, actualChargeForce); - + expectedDischargeForce = false; actualdischargeForce = this.battery.getForceDischargeActive().get(); assertEquals(expectedDischargeForce, actualdischargeForce); } - + @Test - public void testSetMaxAllowedCurrents() { + public void testSetMaxAllowedCurrents() { // Nothing is necessary int maxDischargeCurrentFromBms = DummyBattery.DEFAULT_MAX_DISCHARGE_CURRENT; int maxChargeCurrentFromBms = DummyBattery.DEFAULT_MAX_CHARGE_CURRENT; - SetAllowedCurrents.setMaxAllowedCurrents(this.battery, this.cellCharacteristic, this.settings, maxChargeCurrentFromBms, maxDischargeCurrentFromBms); - + SetAllowedCurrents.setMaxAllowedCurrents(this.battery, this.cellCharacteristic, this.settings, + maxChargeCurrentFromBms, maxDischargeCurrentFromBms); + this.battery.getChargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceDischargeActiveChannel().nextProcessImage(); this.battery.getDischargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceChargeActiveChannel().nextProcessImage(); - + int expectedMaxChargeCurrent = maxChargeCurrentFromBms; int actualMaxChargeCurrent = this.battery.getChargeMaxCurrent().get(); assertEquals(expectedMaxChargeCurrent, actualMaxChargeCurrent); @@ -130,23 +139,25 @@ public void testSetMaxAllowedCurrents() { boolean expectedDischargeForce = false; boolean actualdischargeForce = this.battery.getForceDischargeActive().get(); assertEquals(expectedDischargeForce, actualdischargeForce); - + // Battery has to be charged maxDischargeCurrentFromBms = 0; this.battery.setMinimalCellVoltage(DummyCellCharacteristic.FORCE_CHARGE_CELL_VOLTAGE_MV); - - SetAllowedCurrents.setMaxAllowedCurrents(this.battery, this.cellCharacteristic, this.settings, maxChargeCurrentFromBms, maxDischargeCurrentFromBms); - + + SetAllowedCurrents.setMaxAllowedCurrents(this.battery, this.cellCharacteristic, this.settings, + maxChargeCurrentFromBms, maxDischargeCurrentFromBms); + this.battery.getChargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceDischargeActiveChannel().nextProcessImage(); this.battery.getDischargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceChargeActiveChannel().nextProcessImage(); - + expectedMaxChargeCurrent = maxChargeCurrentFromBms; actualMaxChargeCurrent = this.battery.getChargeMaxCurrent().get(); assertEquals(expectedMaxChargeCurrent, actualMaxChargeCurrent); - - expectedMaxDischargeCurrent = - (int) Math.max(MIN_CURRENT_AMPERE, this.battery.getCapacity().get() * POWER_FACTOR / this.battery.getVoltage().get()); + + expectedMaxDischargeCurrent = -(int) Math.max(MIN_CURRENT_AMPERE, + this.battery.getCapacity().get() * POWER_FACTOR / this.battery.getVoltage().get()); actualMaxDischargeCurrent = this.battery.getDischargeMaxCurrent().get(); assertEquals(expectedMaxDischargeCurrent, actualMaxDischargeCurrent); expectedChargeForce = true; @@ -162,45 +173,45 @@ public void testSetChannelsForCharge() { int expectedCurrent = DummyBattery.DEFAULT_MAX_CHARGE_CURRENT; int actualCurrent = this.battery.getChargeMaxCurrent().get(); assertEquals(expectedCurrent, actualCurrent); - + // Battery can be charged, no discharge necessary int maxChargeCurrent = DummyBattery.DEFAULT_MAX_CHARGE_CURRENT + 1; - SetAllowedCurrents.setChannelsForCharge(maxChargeCurrent, this.battery); + SetAllowedCurrents.setChannelsForCharge(maxChargeCurrent, this.battery); this.battery.getChargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceDischargeActiveChannel().nextProcessImage(); - + expectedCurrent = maxChargeCurrent; - actualCurrent = this.battery.getChargeMaxCurrent().get(); + actualCurrent = this.battery.getChargeMaxCurrent().get(); assertEquals(expectedCurrent, actualCurrent); - + boolean expectedForce = false; boolean actualForce = this.battery.getForceDischargeActive().get(); assertEquals(expectedForce, actualForce); - + // Battery cannot be charged, no discharge necessary maxChargeCurrent = 0; SetAllowedCurrents.setChannelsForCharge(maxChargeCurrent, this.battery); this.battery.getChargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceDischargeActiveChannel().nextProcessImage(); - + expectedCurrent = maxChargeCurrent; - actualCurrent = this.battery.getChargeMaxCurrent().get(); + actualCurrent = this.battery.getChargeMaxCurrent().get(); assertEquals(expectedCurrent, actualCurrent); - + expectedForce = false; actualForce = this.battery.getForceDischargeActive().get(); assertEquals(expectedForce, actualForce); - + // Battery cannot be charged, must be discharged maxChargeCurrent = -8; SetAllowedCurrents.setChannelsForCharge(maxChargeCurrent, this.battery); this.battery.getChargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceDischargeActiveChannel().nextProcessImage(); - + expectedCurrent = maxChargeCurrent; - actualCurrent = this.battery.getChargeMaxCurrent().get(); + actualCurrent = this.battery.getChargeMaxCurrent().get(); assertEquals(expectedCurrent, actualCurrent); - + expectedForce = true; actualForce = this.battery.getForceDischargeActive().get(); assertEquals(expectedForce, actualForce); @@ -211,127 +222,126 @@ public void testSetChannelsForDischarge() { int expectedCurrent = DummyBattery.DEFAULT_MAX_DISCHARGE_CURRENT; int actualCurrent = this.battery.getDischargeMaxCurrent().get(); assertEquals(expectedCurrent, actualCurrent); - + // Battery can be discharged, no charge necessary int maxDischargeCurrent = DummyBattery.DEFAULT_MAX_DISCHARGE_CURRENT + 1; - SetAllowedCurrents.setChannelsForDischarge(maxDischargeCurrent, this.battery); + SetAllowedCurrents.setChannelsForDischarge(maxDischargeCurrent, this.battery); this.battery.getDischargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceChargeActiveChannel().nextProcessImage(); - + expectedCurrent = maxDischargeCurrent; - actualCurrent = this.battery.getDischargeMaxCurrent().get(); + actualCurrent = this.battery.getDischargeMaxCurrent().get(); assertEquals(expectedCurrent, actualCurrent); - + boolean expectedForce = false; boolean actualForce = this.battery.getForceChargeActive().get(); assertEquals(expectedForce, actualForce); - + // Battery cannot be discharged, no charge necessary maxDischargeCurrent = 0; SetAllowedCurrents.setChannelsForDischarge(maxDischargeCurrent, this.battery); this.battery.getDischargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceChargeActiveChannel().nextProcessImage(); - + expectedCurrent = maxDischargeCurrent; - actualCurrent = this.battery.getDischargeMaxCurrent().get(); + actualCurrent = this.battery.getDischargeMaxCurrent().get(); assertEquals(expectedCurrent, actualCurrent); - + expectedForce = false; actualForce = this.battery.getForceChargeActive().get(); assertEquals(expectedForce, actualForce); - + // Battery cannot be charged, must be charged maxDischargeCurrent = -8; SetAllowedCurrents.setChannelsForDischarge(maxDischargeCurrent, this.battery); this.battery.getDischargeMaxCurrentChannel().nextProcessImage(); this.battery.getForceChargeActiveChannel().nextProcessImage(); - + expectedCurrent = maxDischargeCurrent; - actualCurrent = this.battery.getDischargeMaxCurrent().get(); + actualCurrent = this.battery.getDischargeMaxCurrent().get(); assertEquals(expectedCurrent, actualCurrent); - + expectedForce = true; actualForce = this.battery.getForceChargeActive().get(); - assertEquals(expectedForce, actualForce); + assertEquals(expectedForce, actualForce); } - @Test public void testIsVoltageLowerThanForceDischargeVoltage() { - + assertTrue(SetAllowedCurrents.isVoltageLowerThanForceDischargeVoltage(this.cellCharacteristic, this.battery)); - - this.battery.setMaximalCellVoltage((DummyCellCharacteristic.FORCE_DISCHARGE_CELL_VOLTAGE_MV - 1)); + + this.battery.setMaximalCellVoltage((DummyCellCharacteristic.FORCE_DISCHARGE_CELL_VOLTAGE_MV - 1)); assertTrue(SetAllowedCurrents.isVoltageLowerThanForceDischargeVoltage(this.cellCharacteristic, this.battery)); - - this.battery.setMaximalCellVoltage((DummyCellCharacteristic.FORCE_DISCHARGE_CELL_VOLTAGE_MV)); + + this.battery.setMaximalCellVoltage((DummyCellCharacteristic.FORCE_DISCHARGE_CELL_VOLTAGE_MV)); assertFalse(SetAllowedCurrents.isVoltageLowerThanForceDischargeVoltage(this.cellCharacteristic, this.battery)); - - this.battery.setMaximalCellVoltage((DummyCellCharacteristic.FORCE_DISCHARGE_CELL_VOLTAGE_MV + 1)); + + this.battery.setMaximalCellVoltage((DummyCellCharacteristic.FORCE_DISCHARGE_CELL_VOLTAGE_MV + 1)); assertFalse(SetAllowedCurrents.isVoltageLowerThanForceDischargeVoltage(this.cellCharacteristic, this.battery)); } @Test public void testIsVoltageAboveFinalChargingVoltage() { assertFalse(SetAllowedCurrents.isVoltageAboveFinalChargingVoltage(this.cellCharacteristic, this.battery)); - - this.battery.setMaximalCellVoltage((DummyCellCharacteristic.FINAL_CELL_CHARGE_VOLTAGE_MV - 1)); + + this.battery.setMaximalCellVoltage((DummyCellCharacteristic.FINAL_CELL_CHARGE_VOLTAGE_MV - 1)); assertFalse(SetAllowedCurrents.isVoltageAboveFinalChargingVoltage(this.cellCharacteristic, this.battery)); - - this.battery.setMaximalCellVoltage((DummyCellCharacteristic.FINAL_CELL_CHARGE_VOLTAGE_MV)); + + this.battery.setMaximalCellVoltage((DummyCellCharacteristic.FINAL_CELL_CHARGE_VOLTAGE_MV)); assertFalse(SetAllowedCurrents.isVoltageAboveFinalChargingVoltage(this.cellCharacteristic, this.battery)); - - this.battery.setMaximalCellVoltage((DummyCellCharacteristic.FINAL_CELL_CHARGE_VOLTAGE_MV + 1)); - assertTrue(SetAllowedCurrents.isVoltageAboveFinalChargingVoltage(this.cellCharacteristic, this.battery)); + + this.battery.setMaximalCellVoltage((DummyCellCharacteristic.FINAL_CELL_CHARGE_VOLTAGE_MV + 1)); + assertTrue(SetAllowedCurrents.isVoltageAboveFinalChargingVoltage(this.cellCharacteristic, this.battery)); } @Test public void testIsVoltageHigherThanForceChargeVoltage() { assertTrue(SetAllowedCurrents.isVoltageHigherThanForceChargeVoltage(this.cellCharacteristic, this.battery)); - - this.battery.setMinimalCellVoltage((DummyCellCharacteristic.FORCE_CHARGE_CELL_VOLTAGE_MV - 1)); + + this.battery.setMinimalCellVoltage((DummyCellCharacteristic.FORCE_CHARGE_CELL_VOLTAGE_MV - 1)); assertFalse(SetAllowedCurrents.isVoltageHigherThanForceChargeVoltage(this.cellCharacteristic, this.battery)); - - this.battery.setMinimalCellVoltage((DummyCellCharacteristic.FORCE_CHARGE_CELL_VOLTAGE_MV)); + + this.battery.setMinimalCellVoltage((DummyCellCharacteristic.FORCE_CHARGE_CELL_VOLTAGE_MV)); assertFalse(SetAllowedCurrents.isVoltageHigherThanForceChargeVoltage(this.cellCharacteristic, this.battery)); - - this.battery.setMinimalCellVoltage((DummyCellCharacteristic.FORCE_CHARGE_CELL_VOLTAGE_MV + 1)); - assertTrue(SetAllowedCurrents.isVoltageHigherThanForceChargeVoltage(this.cellCharacteristic, this.battery)); + + this.battery.setMinimalCellVoltage((DummyCellCharacteristic.FORCE_CHARGE_CELL_VOLTAGE_MV + 1)); + assertTrue(SetAllowedCurrents.isVoltageHigherThanForceChargeVoltage(this.cellCharacteristic, this.battery)); } @Test public void testIsVoltageBelowFinalDischargingVoltage() { assertFalse(SetAllowedCurrents.isVoltageBelowFinalDischargingVoltage(this.cellCharacteristic, this.battery)); - - this.battery.setMinimalCellVoltage(DummyCellCharacteristic.FINAL_CELL_DISCHARGE_VOLTAGE_MV - 1); + + this.battery.setMinimalCellVoltage(DummyCellCharacteristic.FINAL_CELL_DISCHARGE_VOLTAGE_MV - 1); assertTrue(SetAllowedCurrents.isVoltageBelowFinalDischargingVoltage(this.cellCharacteristic, this.battery)); - - this.battery.setMinimalCellVoltage((DummyCellCharacteristic.FINAL_CELL_DISCHARGE_VOLTAGE_MV)); + + this.battery.setMinimalCellVoltage((DummyCellCharacteristic.FINAL_CELL_DISCHARGE_VOLTAGE_MV)); + assertFalse(SetAllowedCurrents.isVoltageBelowFinalDischargingVoltage(this.cellCharacteristic, this.battery)); + + this.battery.setMinimalCellVoltage((DummyCellCharacteristic.FINAL_CELL_DISCHARGE_VOLTAGE_MV + 1)); assertFalse(SetAllowedCurrents.isVoltageBelowFinalDischargingVoltage(this.cellCharacteristic, this.battery)); - - this.battery.setMinimalCellVoltage((DummyCellCharacteristic.FINAL_CELL_DISCHARGE_VOLTAGE_MV + 1)); - assertFalse(SetAllowedCurrents.isVoltageBelowFinalDischargingVoltage(this.cellCharacteristic, this.battery)); } @Test public void testIsFurtherDischargingNecessary() { assertFalse(SetAllowedCurrents.isFurtherDischargingNecessary(this.cellCharacteristic, this.battery)); - + this.battery.setMaximalCellVoltage(DummyCellCharacteristic.FINAL_CELL_CHARGE_VOLTAGE_MV + 1); assertFalse(SetAllowedCurrents.isFurtherDischargingNecessary(this.cellCharacteristic, this.battery)); - + this.battery.setMaximalCellVoltage(DummyCellCharacteristic.FORCE_DISCHARGE_CELL_VOLTAGE_MV + 1); assertFalse(SetAllowedCurrents.isFurtherDischargingNecessary(this.cellCharacteristic, this.battery)); - + this.battery.setForceDischargeActive(false); assertFalse(SetAllowedCurrents.isFurtherDischargingNecessary(this.cellCharacteristic, this.battery)); - + this.battery.setForceDischargeActive(true); assertTrue(SetAllowedCurrents.isFurtherDischargingNecessary(this.cellCharacteristic, this.battery)); - + this.battery.setMaximalCellVoltage(DummyCellCharacteristic.FINAL_CELL_CHARGE_VOLTAGE_MV + 1); assertTrue(SetAllowedCurrents.isFurtherDischargingNecessary(this.cellCharacteristic, this.battery)); - + this.battery.setMaximalCellVoltage(DummyCellCharacteristic.FINAL_CELL_CHARGE_VOLTAGE_MV); assertFalse(SetAllowedCurrents.isFurtherDischargingNecessary(this.cellCharacteristic, this.battery)); } @@ -339,87 +349,97 @@ public void testIsFurtherDischargingNecessary() { @Test public void testIsDischargingAlready() { assertFalse(SetAllowedCurrents.isDischargingAlready(this.battery)); - + this.battery.setForceDischargeActive(true); assertTrue(SetAllowedCurrents.isDischargingAlready(this.battery)); - + this.battery.setForceDischargeActive(false); assertFalse(SetAllowedCurrents.isDischargingAlready(this.battery)); } @Test - public void testCalculateForceCurrent() { - int expected = - (int) Math.max(MIN_CURRENT_AMPERE, DummyBattery.DEFAULT_CAPACITY * POWER_FACTOR / DummyBattery.DEFAULT_VOLTAGE); // 1.333 => 1 + public void testCalculateForceCurrent() { + int expected = -(int) Math.max(MIN_CURRENT_AMPERE, + DummyBattery.DEFAULT_CAPACITY * POWER_FACTOR / DummyBattery.DEFAULT_VOLTAGE); // 1.333 => 1 assertEquals(expected, SetAllowedCurrents.calculateForceCurrent(this.battery, this.settings)); - + int newCapacity = 200_000; this.battery.setCapacity(newCapacity); - expected = - (int) Math.max(MIN_CURRENT_AMPERE, newCapacity * POWER_FACTOR / DummyBattery.DEFAULT_VOLTAGE); // 5.333 => 5 + // 5.333 => 5 + expected = -(int) Math.max(MIN_CURRENT_AMPERE, newCapacity * POWER_FACTOR / DummyBattery.DEFAULT_VOLTAGE); assertEquals(expected, SetAllowedCurrents.calculateForceCurrent(this.battery, this.settings)); - + int newVoltage = 850; this.battery.setCapacity(newCapacity); this.battery.setVoltage(newVoltage); - expected = - (int) Math.max(MIN_CURRENT_AMPERE, newCapacity * POWER_FACTOR / newVoltage); // 4.706 => 4 + expected = -(int) Math.max(MIN_CURRENT_AMPERE, newCapacity * POWER_FACTOR / newVoltage); // 4.706 => 4 assertEquals(expected, SetAllowedCurrents.calculateForceCurrent(this.battery, this.settings)); - - newCapacity = 30_000; + + newCapacity = 30_000; newVoltage = 700; this.battery.setCapacity(newCapacity); this.battery.setVoltage(newVoltage); - expected = - (int) Math.max(MIN_CURRENT_AMPERE, newCapacity * POWER_FACTOR / newVoltage); // 0.857 => 1 + expected = -(int) Math.max(MIN_CURRENT_AMPERE, newCapacity * POWER_FACTOR / newVoltage); // 0.857 => 1 assertEquals(expected, SetAllowedCurrents.calculateForceCurrent(this.battery, this.settings)); - - newCapacity = 10_000; + + newCapacity = 10_000; this.battery.setCapacity(newCapacity); this.battery.setVoltage(newVoltage); - expected = - (int) Math.max(MIN_CURRENT_AMPERE, newCapacity * POWER_FACTOR / newVoltage); // 0.286 => 1 + expected = -(int) Math.max(MIN_CURRENT_AMPERE, newCapacity * POWER_FACTOR / newVoltage); // 0.286 => 1 assertEquals(expected, SetAllowedCurrents.calculateForceCurrent(this.battery, this.settings)); } - - + @Test public void testCalculateForceDischargeCurrent() { - int expected = - (int) Math.max(MIN_CURRENT_AMPERE, DummyBattery.DEFAULT_CAPACITY * POWER_FACTOR / DummyBattery.DEFAULT_VOLTAGE); // 1.333 => 1 - assertEquals(expected, SetAllowedCurrents.calculateForceDischargeCurrent(this.battery, this.settings)); + int expected = -(int) Math.max(MIN_CURRENT_AMPERE, + DummyBattery.DEFAULT_CAPACITY * POWER_FACTOR / DummyBattery.DEFAULT_VOLTAGE); // 1.333 => 1 + assertEquals(expected, SetAllowedCurrents.calculateForceDischargeCurrent(this.battery, this.settings)); } - + @Test - public void testCalculateForceChargeCurrent() { - int expected = - (int) Math.max(MIN_CURRENT_AMPERE, DummyBattery.DEFAULT_CAPACITY * POWER_FACTOR / DummyBattery.DEFAULT_VOLTAGE); // 1.333 => 1 + public void testCalculateForceChargeCurrent() { + int expected = -(int) Math.max(MIN_CURRENT_AMPERE, + DummyBattery.DEFAULT_CAPACITY * POWER_FACTOR / DummyBattery.DEFAULT_VOLTAGE); // 1.333 => 1 assertEquals(expected, SetAllowedCurrents.calculateForceDischargeCurrent(this.battery, this.settings)); } @Test public void testIsFurtherChargingNecessary() { - assertFalse(SetAllowedCurrents.isFurtherChargingNecessary(this.battery, this.cellCharacteristic, this.settings)); - + assertFalse( + SetAllowedCurrents.isFurtherChargingNecessary(this.battery, this.cellCharacteristic, this.settings)); + this.battery.setMinimalCellVoltage(DummyCellCharacteristic.FINAL_CELL_DISCHARGE_VOLTAGE_MV - 1); - assertFalse(SetAllowedCurrents.isFurtherChargingNecessary(this.battery, this.cellCharacteristic, this.settings)); - + assertFalse( + SetAllowedCurrents.isFurtherChargingNecessary(this.battery, this.cellCharacteristic, this.settings)); + this.battery.setMinimalCellVoltage(DummyCellCharacteristic.FORCE_CHARGE_CELL_VOLTAGE_MV - 1); - assertFalse(SetAllowedCurrents.isFurtherChargingNecessary(this.battery, this.cellCharacteristic, this.settings)); - + assertFalse( + SetAllowedCurrents.isFurtherChargingNecessary(this.battery, this.cellCharacteristic, this.settings)); + this.battery.setForceChargeActive(false); - assertFalse(SetAllowedCurrents.isFurtherChargingNecessary(this.battery, this.cellCharacteristic, this.settings)); - + assertFalse( + SetAllowedCurrents.isFurtherChargingNecessary(this.battery, this.cellCharacteristic, this.settings)); + this.battery.setForceChargeActive(true); assertTrue(SetAllowedCurrents.isFurtherChargingNecessary(this.battery, this.cellCharacteristic, this.settings)); - - this.battery.setMinimalCellVoltage(DummyCellCharacteristic.FINAL_CELL_DISCHARGE_VOLTAGE_MV - this.settings.getToleranceMilliVolt() - 1); + + this.battery.setMinimalCellVoltage( + DummyCellCharacteristic.FINAL_CELL_DISCHARGE_VOLTAGE_MV - this.settings.getToleranceMilliVolt() - 1); assertTrue(SetAllowedCurrents.isFurtherChargingNecessary(this.battery, this.cellCharacteristic, this.settings)); - - this.battery.setMinimalCellVoltage(DummyCellCharacteristic.FINAL_CELL_DISCHARGE_VOLTAGE_MV - this.settings.getToleranceMilliVolt()); - assertFalse(SetAllowedCurrents.isFurtherChargingNecessary(this.battery, this.cellCharacteristic, this.settings)); + + this.battery.setMinimalCellVoltage( + DummyCellCharacteristic.FINAL_CELL_DISCHARGE_VOLTAGE_MV - this.settings.getToleranceMilliVolt()); + assertFalse( + SetAllowedCurrents.isFurtherChargingNecessary(this.battery, this.cellCharacteristic, this.settings)); } @Test public void testIsChargingAlready() { assertFalse(SetAllowedCurrents.isChargingAlready(this.battery)); - + this.battery.setForceChargeActive(true); assertTrue(SetAllowedCurrents.isChargingAlready(this.battery)); - + this.battery.setForceChargeActive(false); assertFalse(SetAllowedCurrents.isChargingAlready(this.battery)); } @@ -427,23 +447,23 @@ public void testIsChargingAlready() { @Test public void testAreApiValuesPresent() { assertTrue(SetAllowedCurrents.areApiValuesPresent(this.battery)); - - this.battery.setCapacityToUndefined(); - assertFalse(SetAllowedCurrents.areApiValuesPresent(this.battery)); - - this.battery.setCapacity(DummyBattery.DEFAULT_CAPACITY); - this.battery.setVoltageToUndefined(); - assertFalse(SetAllowedCurrents.areApiValuesPresent(this.battery)); - - this.battery.setVoltage(DummyBattery.DEFAULT_VOLTAGE); - this.battery.setMinimalCellVoltageToUndefined(); - assertFalse(SetAllowedCurrents.areApiValuesPresent(this.battery)); - - this.battery.setMinimalCellVoltage(DummyBattery.DEFAULT_MIN_CELL_VOLTAGE); - this.battery.setMaximalCellVoltageToUndefined(); - assertFalse(SetAllowedCurrents.areApiValuesPresent(this.battery)); - - this.battery.setMaximalCellVoltage(DummyBattery.DEFAULT_MAX_CELL_VOLTAGE); - assertTrue(SetAllowedCurrents.areApiValuesPresent(this.battery)); + + this.battery.setCapacityToUndefined(); + assertFalse(SetAllowedCurrents.areApiValuesPresent(this.battery)); + + this.battery.setCapacity(DummyBattery.DEFAULT_CAPACITY); + this.battery.setVoltageToUndefined(); + assertFalse(SetAllowedCurrents.areApiValuesPresent(this.battery)); + + this.battery.setVoltage(DummyBattery.DEFAULT_VOLTAGE); + this.battery.setMinimalCellVoltageToUndefined(); + assertFalse(SetAllowedCurrents.areApiValuesPresent(this.battery)); + + this.battery.setMinimalCellVoltage(DummyBattery.DEFAULT_MIN_CELL_VOLTAGE); + this.battery.setMaximalCellVoltageToUndefined(); + assertFalse(SetAllowedCurrents.areApiValuesPresent(this.battery)); + + this.battery.setMaximalCellVoltage(DummyBattery.DEFAULT_MAX_CELL_VOLTAGE); + assertTrue(SetAllowedCurrents.areApiValuesPresent(this.battery)); } } \ No newline at end of file diff --git a/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/BatteryProtectionTest.java b/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/BatteryProtectionTest.java new file mode 100644 index 00000000000..489e3ac0040 --- /dev/null +++ b/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/BatteryProtectionTest.java @@ -0,0 +1,257 @@ +package io.openems.edge.battery.protection; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; + +import org.junit.Test; + +import io.openems.common.channel.Unit; +import io.openems.common.types.ChannelAddress; +import io.openems.common.types.OpenemsType; +import io.openems.edge.battery.protection.currenthandler.ChargeMaxCurrentHandler; +import io.openems.edge.battery.protection.currenthandler.DischargeMaxCurrentHandler; +import io.openems.edge.battery.protection.force.ForceCharge; +import io.openems.edge.battery.protection.force.ForceDischarge; +import io.openems.edge.battery.test.DummyBattery; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.linecharacteristic.PolyLine; +import io.openems.edge.common.startstop.StartStop; +import io.openems.edge.common.startstop.StartStoppable; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.common.test.ComponentTest; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.TimeLeapClock; + +public class BatteryProtectionTest { + + public static final double MAX_INCREASE_AMPERE_PER_SECOND = 0.5; + + public static final PolyLine CHARGE_VOLTAGE_TO_PERCENT = PolyLine.create() // + .addPoint(3000, 0.1) // + .addPoint(Math.nextUp(3000), 1) // + .addPoint(3350, 1) // + .addPoint(3450, 0.9999) // + .addPoint(3600, 0.02) // + .addPoint(Math.nextDown(3650), 0.02) // + .addPoint(3650, 0) // + .build(); + + public static final PolyLine CHARGE_TEMPERATURE_TO_PERCENT = PolyLine.create() // + .addPoint(Math.nextDown(-10), 0) // + .addPoint(-10, 0.215) // + .addPoint(0, 0.215) // + .addPoint(1, 0.325) // + .addPoint(5, 0.325) // + .addPoint(6, 0.65) // + .addPoint(15, 0.65) // + .addPoint(16, 1) // + .addPoint(44, 1) // + .addPoint(45, 0.65) // + .addPoint(49, 0.65) // + .addPoint(50, 0.325) // + .addPoint(54, 0.325) // + .addPoint(55, 0) // + .build(); + + public static final ForceDischarge.Params FORCE_DISCHARGE = new ForceDischarge.Params(3660, 3640, 3450); + + public static final PolyLine DISCHARGE_VOLTAGE_TO_PERCENT = PolyLine.create() // + .addPoint(2900, 0) // + .addPoint(Math.nextUp(2900), 0.05) // + .addPoint(2920, 0.05) // + .addPoint(3000, 1) // + .addPoint(3700, 1) // + .addPoint(Math.nextUp(3700), 0) // + .build(); + + public static final PolyLine DISCHARGE_TEMPERATURE_TO_PERCENT = PolyLine.create() // + .addPoint(Math.nextDown(-10), 0) // + .addPoint(-10, 0.215) // + .addPoint(0, 0.215) // + .addPoint(1, 1) // + .addPoint(44, 1) // + .addPoint(45, 0.865) // + .addPoint(49, 0.865) // + .addPoint(50, 0.325) // + .addPoint(54, 0.325) // + .addPoint(55, 0) // + .build(); + + public static final ForceCharge.Params FORCE_CHARGE = new ForceCharge.Params(2850, 2910, 3000); + + public static final int INITIAL_BMS_MAX_EVER_CURRENT = 80; + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + ORIGINAL_CHARGE_MAX_CURRENT(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.AMPERE)), // + ORIGINAL_DISCHARGE_MAX_CURRENT(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.AMPERE)); // + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + + private static final String BATTERY_ID = "battery0"; + + private static final ChannelAddress BATTERY_START_STOP = new ChannelAddress(BATTERY_ID, + StartStoppable.ChannelId.START_STOP.id()); + private static final ChannelAddress BATTERY_BP_CHARGE_BMS = new ChannelAddress(BATTERY_ID, + BatteryProtection.ChannelId.BP_CHARGE_BMS.id()); + private static final ChannelAddress BATTERY_BP_DISCHARGE_BMS = new ChannelAddress(BATTERY_ID, + BatteryProtection.ChannelId.BP_DISCHARGE_BMS.id()); + private static final ChannelAddress BATTERY_MIN_CELL_VOLTAGE = new ChannelAddress(BATTERY_ID, "MinCellVoltage"); + private static final ChannelAddress BATTERY_MAX_CELL_VOLTAGE = new ChannelAddress(BATTERY_ID, "MaxCellVoltage"); + private static final ChannelAddress BATTERY_MIN_CELL_TEMPERATURE = new ChannelAddress(BATTERY_ID, + "MinCellTemperature"); + private static final ChannelAddress BATTERY_MAX_CELL_TEMPERATURE = new ChannelAddress(BATTERY_ID, + "MaxCellTemperature"); + private static final ChannelAddress BATTERY_CHARGE_MAX_CURRENT = new ChannelAddress(BATTERY_ID, "ChargeMaxCurrent"); + private static final ChannelAddress BATTERY_DISCHARGE_MAX_CURRENT = new ChannelAddress(BATTERY_ID, + "DischargeMaxCurrent"); + + @Test + public void test() throws Exception { + final DummyBattery battery = new DummyBattery(BATTERY_ID, BatteryProtection.ChannelId.values()); + final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final DummyComponentManager cm = new DummyComponentManager(clock); + final BatteryProtection sut = BatteryProtection.create(battery) // + .setChargeMaxCurrentHandler(ChargeMaxCurrentHandler.create(cm, INITIAL_BMS_MAX_EVER_CURRENT) // + .setVoltageToPercent(CHARGE_VOLTAGE_TO_PERCENT) // + .setTemperatureToPercent(CHARGE_TEMPERATURE_TO_PERCENT) // + .setMaxIncreasePerSecond(MAX_INCREASE_AMPERE_PER_SECOND) // + .setForceDischarge(FORCE_DISCHARGE) // + .build()) // + .setDischargeMaxCurrentHandler(DischargeMaxCurrentHandler.create(cm, INITIAL_BMS_MAX_EVER_CURRENT) // + .setVoltageToPercent(DISCHARGE_VOLTAGE_TO_PERCENT) + .setTemperatureToPercent(DISCHARGE_TEMPERATURE_TO_PERCENT) // + .setMaxIncreasePerSecond(MAX_INCREASE_AMPERE_PER_SECOND) // + .setForceCharge(FORCE_CHARGE) // + .build()) // + .build(); + new ComponentTest(new DummyBattery(BATTERY_ID)) // + .addComponent(battery) // + .next(new TestCase() // + .input(BATTERY_START_STOP, StartStop.START) // + .input(BATTERY_BP_CHARGE_BMS, 80) // + .input(BATTERY_BP_DISCHARGE_BMS, 80) // + .input(BATTERY_MIN_CELL_VOLTAGE, 2950) // + .input(BATTERY_MAX_CELL_VOLTAGE, 3300) // + .input(BATTERY_MIN_CELL_TEMPERATURE, 16) // + .input(BATTERY_MAX_CELL_TEMPERATURE, 17) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 0) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 0)) // + .next(new TestCase("open, but maxIncreaseAmpereLimit") // + .timeleap(clock, 2, ChronoUnit.SECONDS) // + .input(BATTERY_MIN_CELL_VOLTAGE, 3000) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 1) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 1)) // + .next(new TestCase() // + .timeleap(clock, 2, ChronoUnit.SECONDS) // + .input(BATTERY_MIN_CELL_VOLTAGE, 3050) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 2) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 2)) // + .next(new TestCase() // + .timeleap(clock, 10, ChronoUnit.SECONDS) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 7) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 7)) // + .next(new TestCase() // + .timeleap(clock, 10, ChronoUnit.MINUTES) // + .input(BATTERY_MAX_CELL_VOLTAGE, 3300) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 80) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .next(new TestCase() // + .timeleap(clock, 10, ChronoUnit.MINUTES) // + .input(BATTERY_MAX_CELL_VOLTAGE, 3499) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 54) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .next(new TestCase() // + .timeleap(clock, 10, ChronoUnit.MINUTES) // + .input(BATTERY_MAX_CELL_VOLTAGE, 3649) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 2) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .next(new TestCase() // + .timeleap(clock, 10, ChronoUnit.MINUTES) // + .input(BATTERY_MAX_CELL_VOLTAGE, 3649) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 2) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .next(new TestCase() // + .timeleap(clock, 10, ChronoUnit.MINUTES) // + .input(BATTERY_MAX_CELL_VOLTAGE, 3650) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 0) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .next(new TestCase("Start Force-Discharge: wait 60 seconds") // + .timeleap(clock, 1, ChronoUnit.SECONDS) // + .input(BATTERY_MAX_CELL_VOLTAGE, 3660) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 0) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .next(new TestCase("Start Force-Discharge") // + .timeleap(clock, 60, ChronoUnit.SECONDS) // + .input(BATTERY_MAX_CELL_VOLTAGE, 3660) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, -1) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .next(new TestCase("Force-Discharge") // + .timeleap(clock, 1, ChronoUnit.SECONDS) // + .input(BATTERY_MAX_CELL_VOLTAGE, 3640) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, -1) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .next(new TestCase("Block Charge #1") // + .timeleap(clock, 1, ChronoUnit.SECONDS) // + .input(BATTERY_MAX_CELL_VOLTAGE, 3639) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 0) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .next(new TestCase("Block Charge #2") // + .timeleap(clock, 1, ChronoUnit.SECONDS) // + .input(BATTERY_MAX_CELL_VOLTAGE, 3600) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 0) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .next(new TestCase("Block Charge #3") // + .timeleap(clock, 1, ChronoUnit.SECONDS) // + .input(BATTERY_MAX_CELL_VOLTAGE, 3450) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 0) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .next(new TestCase("Finish Force-Discharge") // + .timeleap(clock, 1, ChronoUnit.SECONDS) // + .input(BATTERY_MAX_CELL_VOLTAGE, 3449) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 0) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .next(new TestCase() // + .timeleap(clock, 1, ChronoUnit.SECONDS) // + .input(BATTERY_MAX_CELL_VOLTAGE, 3400) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 0) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + .next(new TestCase("Allow Charge") // + .timeleap(clock, 1, ChronoUnit.SECONDS) // + .input(BATTERY_MAX_CELL_VOLTAGE, 3350) // + .onAfterProcessImage(() -> sut.apply()) // + .output(BATTERY_CHARGE_MAX_CURRENT, 1) // + .output(BATTERY_DISCHARGE_MAX_CURRENT, 80)) // + ; + } + +} diff --git a/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/currenthandler/MaxCurrentHandlerTest.java b/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/currenthandler/MaxCurrentHandlerTest.java new file mode 100644 index 00000000000..fe0b608391c --- /dev/null +++ b/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/currenthandler/MaxCurrentHandlerTest.java @@ -0,0 +1,196 @@ +package io.openems.edge.battery.protection.currenthandler; + +import static org.junit.Assert.assertEquals; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; + +import org.junit.Test; + +import io.openems.edge.battery.protection.BatteryProtectionTest; +import io.openems.edge.common.linecharacteristic.PolyLine; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.TimeLeapClock; + +public class MaxCurrentHandlerTest { + + @Test + public void testGetMinCellVoltageToPercentLimit() { + final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final DummyComponentManager cm = new DummyComponentManager(clock); + ChargeMaxCurrentHandler sut = ChargeMaxCurrentHandler + .create(cm, BatteryProtectionTest.INITIAL_BMS_MAX_EVER_CURRENT) // + .setVoltageToPercent(BatteryProtectionTest.CHARGE_VOLTAGE_TO_PERCENT) // + .build(); + assertEquals(80, (double) sut.getMinCellVoltageToPercentLimit(3001), 0.1); + assertEquals(8, (double) sut.getMinCellVoltageToPercentLimit(2960), 0.1); + assertEquals(80, (double) sut.getMinCellVoltageToPercentLimit(3001), 0.1); + assertEquals(53.867, (double) sut.getMinCellVoltageToPercentLimit(3500), 0.1); + assertEquals(1.6, (double) sut.getMinCellVoltageToPercentLimit(3600), 0.1); + // Limit is not opened up + assertEquals(1.6, (double) sut.getMinCellVoltageToPercentLimit(3500), 0.1); + assertEquals(0, (double) sut.getMinCellVoltageToPercentLimit(3700), 0.1); + } + + @Test + public void testGetMaxCellVoltageToPercentLimit() { + final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final DummyComponentManager cm = new DummyComponentManager(clock); + ChargeMaxCurrentHandler sut = ChargeMaxCurrentHandler + .create(cm, BatteryProtectionTest.INITIAL_BMS_MAX_EVER_CURRENT) // + .setVoltageToPercent(BatteryProtectionTest.CHARGE_VOLTAGE_TO_PERCENT) // + .build(); + assertEquals(80, (double) sut.getMinCellVoltageToPercentLimit(3450), 0.1); + assertEquals(74.7, (double) sut.getMinCellVoltageToPercentLimit(3460), 0.1); + assertEquals(69.5, (double) sut.getMinCellVoltageToPercentLimit(3470), 0.1); + } + + @Test + public void testDischargeOpenUpLimit() { + final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final DummyComponentManager cm = new DummyComponentManager(clock); + DischargeMaxCurrentHandler sut = DischargeMaxCurrentHandler + .create(cm, BatteryProtectionTest.INITIAL_BMS_MAX_EVER_CURRENT) // + .setVoltageToPercent(BatteryProtectionTest.DISCHARGE_VOLTAGE_TO_PERCENT) // + .build(); + assertEquals(32.5, (double) sut.getMinCellVoltageToPercentLimit(2950), 0.1); + assertEquals(4, (double) sut.getMinCellVoltageToPercentLimit(2920), 0.1); + assertEquals(4, (double) sut.getMinCellVoltageToPercentLimit(2901), 0.1); + assertEquals(0, (double) sut.getMinCellVoltageToPercentLimit(2900), 0.1); + assertEquals(0, (double) sut.getMinCellVoltageToPercentLimit(2899), 0.1); + assertEquals(0, (double) sut.getMinCellVoltageToPercentLimit(2900), 0.1); + assertEquals(0, (double) sut.getMinCellVoltageToPercentLimit(2901), 0.1); + assertEquals(0, (double) sut.getMinCellVoltageToPercentLimit(2950), 0.1); + assertEquals(0, (double) sut.getMinCellVoltageToPercentLimit(2950), 0.1); + assertEquals(80, (double) sut.getMinCellVoltageToPercentLimit(3000), 0.1); + } + + @Test + public void testChargeOpenUpLimit() { + final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final DummyComponentManager cm = new DummyComponentManager(clock); + ChargeMaxCurrentHandler sut = ChargeMaxCurrentHandler + .create(cm, BatteryProtectionTest.INITIAL_BMS_MAX_EVER_CURRENT) // + .setVoltageToPercent(BatteryProtectionTest.CHARGE_VOLTAGE_TO_PERCENT) // + .build(); + assertEquals(80, (double) sut.getMaxCellVoltageToPercentLimit(3400), 0.1); + assertEquals(80, (double) sut.getMaxCellVoltageToPercentLimit(3450), 0.1); + assertEquals(53.9, (double) sut.getMaxCellVoltageToPercentLimit(3500), 0.1); + assertEquals(27.7, (double) sut.getMaxCellVoltageToPercentLimit(3550), 0.1); + assertEquals(1.6, (double) sut.getMaxCellVoltageToPercentLimit(3600), 0.1); + assertEquals(1.6, (double) sut.getMaxCellVoltageToPercentLimit(3620), 0.1); + // smallest ever is "0" + assertEquals(0, (double) sut.getMaxCellVoltageToPercentLimit(3650), 0.1); + assertEquals(0, (double) sut.getMaxCellVoltageToPercentLimit(3620), 0.1); + assertEquals(0, (double) sut.getMaxCellVoltageToPercentLimit(3600), 0.1); + assertEquals(0, (double) sut.getMaxCellVoltageToPercentLimit(3550), 0.1); + assertEquals(0, (double) sut.getMaxCellVoltageToPercentLimit(3500), 0.1); + assertEquals(0, (double) sut.getMaxCellVoltageToPercentLimit(3450), 0.1); + assertEquals(0, (double) sut.getMaxCellVoltageToPercentLimit(3400), 0.1); + // Open up fully only at 3350 mV + assertEquals(80, (double) sut.getMaxCellVoltageToPercentLimit(3350), 0.1); + + assertEquals(80, (double) sut.getMaxCellVoltageToPercentLimit(3400), 0.1); + assertEquals(80, (double) sut.getMaxCellVoltageToPercentLimit(3450), 0.1); + assertEquals(53.9, (double) sut.getMaxCellVoltageToPercentLimit(3500), 0.1); + assertEquals(27.7, (double) sut.getMaxCellVoltageToPercentLimit(3550), 0.1); + assertEquals(1.6, (double) sut.getMaxCellVoltageToPercentLimit(3600), 0.1); + // smallest ever is "1.6" + assertEquals(1.6, (double) sut.getMaxCellVoltageToPercentLimit(3620), 0.1); + assertEquals(1.6, (double) sut.getMaxCellVoltageToPercentLimit(3550), 0.1); + assertEquals(1.6, (double) sut.getMaxCellVoltageToPercentLimit(3500), 0.1); + assertEquals(1.6, (double) sut.getMaxCellVoltageToPercentLimit(3450), 0.1); + assertEquals(1.6, (double) sut.getMaxCellVoltageToPercentLimit(3400), 0.1); + // Open up fully only at 3350 mV + assertEquals(80, (double) sut.getMaxCellVoltageToPercentLimit(3350), 0.1); + } + + @Test + public void testForceDischarge() { + final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final DummyComponentManager cm = new DummyComponentManager(clock); + ChargeMaxCurrentHandler sut = ChargeMaxCurrentHandler.create(cm, 40) // + .setForceDischarge(3660, 3640, 3450) // + .build(); + // Before Force-Discharge limit -> no force discharge + assertEquals(null, sut.getForceCurrent(3000, 3650)); + // Start WAIT_FOR_FORCE_MODE (60 seconds) -> no force discharge + assertEquals(null, sut.getForceCurrent(3000, 3660)); + clock.leap(1, ChronoUnit.MINUTES); + // Enter FORCE_MODE -> force discharge + assertEquals(-1., sut.getForceCurrent(3000, 3660), 0.001); + clock.leap(1, ChronoUnit.SECONDS); + assertEquals(-1., sut.getForceCurrent(3000, 3650), 0.001); + // Enter BLOCK_MODE -> no charge/discharge + assertEquals(0, sut.getForceCurrent(3000, 3639), 0.001); + assertEquals(0, sut.getForceCurrent(3000, 3600), 0.001); + assertEquals(0, sut.getForceCurrent(3000, 3500), 0.001); + // Ende Force-Discharge Mode + assertEquals(null, sut.getForceCurrent(3000, 3449)); + } + + @Test + public void testMaxIncreasePerSecond() { + final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final DummyComponentManager cm = new DummyComponentManager(clock); + ChargeMaxCurrentHandler sut = ChargeMaxCurrentHandler.create(cm, 40) // + .setMaxIncreasePerSecond(BatteryProtectionTest.MAX_INCREASE_AMPERE_PER_SECOND) // + .build(); + sut.lastCurrentLimit = 0.; + sut.lastResultTimestamp = Instant.now(clock); + + clock.leap(1, ChronoUnit.SECONDS); + assertEquals(0.5, (double) sut.getMaxIncreaseAmpereLimit(), 0.001); + sut.lastCurrentLimit = 0.5; + + clock.leap(1, ChronoUnit.SECONDS); + assertEquals(1, (double) sut.getMaxIncreaseAmpereLimit(), 0.001); + sut.lastCurrentLimit = 1.; + + clock.leap(800, ChronoUnit.MILLIS); + assertEquals(1.4, (double) sut.getMaxIncreaseAmpereLimit(), 0.001); + } + + @Test + public void testChargeVoltageToPercent() { + PolyLine p = BatteryProtectionTest.CHARGE_VOLTAGE_TO_PERCENT; + assertEquals(0.1, p.getValue(2500), 0.001); + assertEquals(0.1, p.getValue(3000), 0.001); + assertEquals(1, p.getValue(Math.nextUp(3000)), 0.001); + assertEquals(1, p.getValue(3450), 0.001); + assertEquals(0.673, p.getValue(3500), 0.001); + assertEquals(0.02, p.getValue(3600), 0.001); + assertEquals(0.02, p.getValue(Math.nextDown(3650)), 0.001); + assertEquals(0, p.getValue(3650), 0.001); + assertEquals(0, p.getValue(4000), 0.001); + } + + @Test + public void testChargeTemperatureToPercent() { + PolyLine p = BatteryProtectionTest.CHARGE_TEMPERATURE_TO_PERCENT; + assertEquals(0, p.getValue(-20), 0.001); + assertEquals(0, p.getValue(Math.nextDown(-10)), 0.001); + assertEquals(0.215, p.getValue(-10), 0.001); + assertEquals(0.215, p.getValue(0), 0.001); + assertEquals(0.27, p.getValue(0.5), 0.001); + assertEquals(0.325, p.getValue(1), 0.001); + assertEquals(0.325, p.getValue(5), 0.001); + assertEquals(0.4875, p.getValue(5.5), 0.001); + assertEquals(0.65, p.getValue(6), 0.001); + assertEquals(0.65, p.getValue(15), 0.001); + assertEquals(0.825, p.getValue(15.5), 0.001); + assertEquals(1, p.getValue(16), 0.001); + assertEquals(1, p.getValue(44), 0.001); + assertEquals(0.825, p.getValue(44.5), 0.001); + assertEquals(0.65, p.getValue(45), 0.001); + assertEquals(0.65, p.getValue(49), 0.001); + assertEquals(0.4875, p.getValue(49.5), 0.001); + assertEquals(0.325, p.getValue(50), 0.001); + assertEquals(0.325, p.getValue(54), 0.001); + assertEquals(0.1625, p.getValue(54.5), 0.001); + assertEquals(0, p.getValue(55), 0.001); + assertEquals(0, p.getValue(100), 0.001); + } + +} diff --git a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/BatteryProtectionDefinitionSoltaro.java b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/BatteryProtectionDefinitionSoltaro.java new file mode 100644 index 00000000000..a282ed61bba --- /dev/null +++ b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/BatteryProtectionDefinitionSoltaro.java @@ -0,0 +1,83 @@ +package io.openems.edge.battery.soltaro; + +import io.openems.edge.battery.protection.BatteryProtectionDefinition; +import io.openems.edge.battery.protection.force.ForceCharge; +import io.openems.edge.battery.protection.force.ForceDischarge; +import io.openems.edge.common.linecharacteristic.PolyLine; + +public class BatteryProtectionDefinitionSoltaro implements BatteryProtectionDefinition { + + @Override + public int getInitialBmsMaxEverChargeCurrent() { + return 80; // [A] + } + + @Override + public int getInitialBmsMaxEverDischargeCurrent() { + return 80; // [A] + } + + @Override + public PolyLine getChargeVoltageToPercent() { + return PolyLine.create() // + .addPoint(3000, 0.1) // + .addPoint(Math.nextUp(3000), 1) // + .addPoint(3350, 1) // + .addPoint(3450, 0.9999) // + .addPoint(3600, 0.02) // + .addPoint(Math.nextDown(3650), 0.02) // + .addPoint(3650, 0) // + .build(); + } + + @Override + public PolyLine getDischargeVoltageToPercent() { + return PolyLine.create() // + .addPoint(2900, 0) // + .addPoint(Math.nextUp(2900), 0.05) // + .addPoint(2920, 0.05) // + .addPoint(3000, 1) // + .addPoint(3700, 1) // + .addPoint(Math.nextUp(3700), 0) // + .build(); + } + + @Override + public PolyLine getChargeTemperatureToPercent() { + return PolyLine.create() // + .addPoint(0, 0) // + .addPoint(Math.nextUp(0), 0.01) // + .addPoint(18, 1) // + .addPoint(35, 1) // + .addPoint(Math.nextDown(40), 0.01) // + .addPoint(40, 0) // + .build(); + } + + @Override + public PolyLine getDischargeTemperatureToPercent() { + return PolyLine.create() // + .addPoint(0, 0) // + .addPoint(Math.nextUp(0), 0.01) // + .addPoint(12, 1) // + .addPoint(45, 1) // + .addPoint(Math.nextDown(55), 0.01) // + .addPoint(55, 0) // + .build(); + } + + @Override + public ForceDischarge.Params getForceDischargeParams() { + return new ForceDischarge.Params(3660, 3640, 3450); + } + + @Override + public ForceCharge.Params getForceChargeParams() { + return new ForceCharge.Params(2850, 2910, 3000); + } + + @Override + public Double getMaxIncreaseAmperePerSecond() { + return 0.1; // [A] per second + } +} \ No newline at end of file diff --git a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionb/ClusterVersionB.java b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionb/ClusterVersionB.java index bf1e1806c0a..1a3d6c6d33f 100644 --- a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionb/ClusterVersionB.java +++ b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/cluster/versionb/ClusterVersionB.java @@ -68,8 +68,7 @@ property = { // EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, // EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE // - } -) + }) public class ClusterVersionB extends AbstractOpenemsModbusComponent implements SoltaroCluster, Battery, OpenemsComponent, EventHandler, ModbusSlave, StartStoppable { @@ -160,7 +159,7 @@ void activate(ComponentContext context, Config config) throws OpenemsException { this.config.numberOfSlaves() * ModuleParameters.MIN_VOLTAGE_MILLIVOLT.getValue() / 1000); this._setCapacity( this.config.racks().length * this.config.numberOfSlaves() * this.config.moduleType().getCapacity_Wh()); - + this.clusterSettings.setNumberOfUsedRacks(calculateUsedRacks(config)); } @@ -180,7 +179,7 @@ public void handleEvent(Event event) { return; } switch (event.getTopic()) { - + case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE: this.setAllowedCurrents.act(); @@ -397,6 +396,7 @@ private boolean haveAllRacksTheSameContactorControlState(ContactorControl cctrl) * Checks whether system has an undefined state, e.g. rack 1 & 2 are configured, * but only rack 1 is running. This state can only be reached at startup coming * from state undefined + * * @return boolean */ private boolean isSystemStatePending() { @@ -411,16 +411,15 @@ private boolean isSystemStatePending() { return b && !this.isSystemRunning() && !this.isSystemStopped(); } - @Override - public String debugLog() { - return "SoC:" + this.getSoc() // - + "|Discharge:" + this.getDischargeMinVoltage() + ";" + this.getDischargeMaxCurrent() // - + "|Charge:" + this.getChargeMaxVoltage() + ";" + this.getChargeMaxCurrent() - + "|Running: " + this.isSystemRunning() - + "|U: " + this.getVoltage() - + "|I: " + this.getCurrent() - ; - } + @Override + public String debugLog() { + return "SoC:" + this.getSoc() // + + "|Discharge:" + this.getDischargeMinVoltage() + ";" + this.getDischargeMaxCurrent() // + + "|Charge:" + this.getChargeMaxVoltage() + ";" + this.getChargeMaxCurrent() // + + "|Running: " + this.isSystemRunning() // + + "|U: " + this.getVoltage() // + + "|I: " + this.getCurrent(); + } private void sleepSystem() { // Write sleep and reset to all racks @@ -487,6 +486,7 @@ private void stopSystem() { /** * Gets the ModbusBridgeId. + * * @return String */ public String getModbusBridgeId() { @@ -495,6 +495,7 @@ public String getModbusBridgeId() { /** * Gets the StateMachineState. + * * @return State */ public State getStateMachineState() { @@ -503,6 +504,7 @@ public State getStateMachineState() { /** * Sets the StateMachineState. + * * @param state the {@link State} */ public void setStateMachineState(State state) { @@ -615,8 +617,8 @@ protected ModbusProtocol defineModbusProtocol() throws OpenemsException { new DummyRegisterElement(0x104B, 0x104D), // m(SoltaroCluster.ChannelId.CLUSTER_MAX_ALLOWED_CHARGE_CURRENT, new UnsignedWordElement(0x104E), ElementToChannelConverter.SCALE_FACTOR_2), // - m(SoltaroCluster.ChannelId.CLUSTER_MAX_ALLOWED_DISCHARGE_CURRENT, new UnsignedWordElement(0x104F), - ElementToChannelConverter.SCALE_FACTOR_2) // + m(SoltaroCluster.ChannelId.CLUSTER_MAX_ALLOWED_DISCHARGE_CURRENT, + new UnsignedWordElement(0x104F), ElementToChannelConverter.SCALE_FACTOR_2) // ), // new FC3ReadRegistersTask(0x1081, Priority.LOW, // diff --git a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionb/SingleRackVersionB.java b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionb/SingleRackVersionB.java index 31ee335eeb8..cbeac1385b1 100644 --- a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionb/SingleRackVersionB.java +++ b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionb/SingleRackVersionB.java @@ -27,205 +27,229 @@ import io.openems.edge.common.startstop.StartStoppable; public interface SingleRackVersionB extends Battery, OpenemsComponent, StartStoppable { - + /** * Gets the ContactorControlChannel. + * * @return WriteChannel */ public default WriteChannel getContactorControlChannel() { return this.channel(ChannelId.BMS_CONTACTOR_CONTROL); } - + /** * Gets the ContactorControl. + * * @return ContactorControl */ public default ContactorControl getContactorControl() { return this.getContactorControlChannel().value().asEnum(); } - + /** * Sets the ContactorControl. + * * @param value the value */ public default void _setContactorControl(ContactorControl value) { this.getContactorControlChannel().setNextValue(value); } - + /** * Sets the ContactorControl. + * * @param value the value * @throws OpenemsNamedException the Exception */ public default void setContactorControl(ContactorControl value) throws OpenemsNamedException { this.getContactorControlChannel().setNextWriteValue(value); } - + /** * Gets the SystemResetChannel. + * * @return IntegerWriteChannel */ public default IntegerWriteChannel getSystemResetChannel() { return this.channel(ChannelId.SYSTEM_RESET); } - + /** * Gets the SystemReset. + * * @return Value */ public default Value getSystemReset() { return this.getSystemResetChannel().value(); } - + /** * Sets the SystemReset. + * * @param value the value */ public default void _setSystemReset(Integer value) { this.getSystemResetChannel().setNextValue(value); } - + /** * Sets the SystemReset. + * * @param value the value * @throws OpenemsNamedException the exception */ public default void setSystemReset(Integer value) throws OpenemsNamedException { this.getSystemResetChannel().setNextWriteValue(value); } - + /** * Gets the SleepChannel. + * * @return IntegerWriteChannel */ public default IntegerWriteChannel getSleepChannel() { return this.channel(ChannelId.SLEEP); } - + /** * Gets the Sleep. + * * @return Value */ public default Value getSleep() { return this.getSleepChannel().value(); } - + /** * Sets the Sleep. + * * @param value Integer */ public default void _setSleep(Integer value) { this.getSleepChannel().setNextValue(value); } - + /** * Sets the Sleep. + * * @param value Integer * @throws OpenemsNamedException the exception */ public default void setSleep(Integer value) throws OpenemsNamedException { this.getSleepChannel().setNextWriteValue(value); } - + /** * Gets the SocLowProtectionChannel. + * * @return IntegerWriteChannel */ public default IntegerWriteChannel getSocLowProtectionChannel() { return this.channel(ChannelId.STOP_PARAMETER_SOC_LOW_PROTECTION); } - + /** * Gets the SocLowProtection. + * * @return Value */ public default Value getSocLowProtection() { return this.getSocLowProtectionChannel().value(); } - + /** * Sets SocLowProtection. + * * @param value Integer */ public default void _setSocLowProtection(Integer value) { this.getSocLowProtectionChannel().setNextValue(value); } - + /** * Sets SocLowProtection. + * * @param value Integer * @throws OpenemsNamedException the exception */ public default void setSocLowProtection(Integer value) throws OpenemsNamedException { this.getSocLowProtectionChannel().setNextWriteValue(value); } - + /** * Gets the SocLowProtectionRecoverChannel. + * * @return IntegerWriteChannel */ public default IntegerWriteChannel getSocLowProtectionRecoverChannel() { return this.channel(ChannelId.STOP_PARAMETER_SOC_LOW_PROTECTION_RECOVER); } - + /** * Gets the SocLowProtectionRecover. + * * @return Value */ public default Value getSocLowProtectionRecover() { return this.getSocLowProtectionRecoverChannel().value(); } - + /** * Sets the SocLowProtectionRecover. + * * @param value Integer */ public default void _setSocLowProtectionRecover(Integer value) { this.getSocLowProtectionRecoverChannel().setNextValue(value); } - + /** * Sets the SocLowProtectionRecover. + * * @param value Integer * @throws OpenemsNamedException OpenemsNamedException */ public default void setSocLowProtectionRecover(Integer value) throws OpenemsNamedException { this.getSocLowProtectionRecoverChannel().setNextWriteValue(value); } - + /** * Gets the WatchdogChannel. + * * @return IntegerWriteChannel */ public default IntegerWriteChannel getWatchdogChannel() { return this.channel(ChannelId.EMS_COMMUNICATION_TIMEOUT); } - + /** * Gets the Watchdog. + * * @return Value */ public default Value getWatchdog() { return this.getWatchdogChannel().value(); } - + /** * Sets the Watchdog. + * * @param value Integer */ public default void _setWatchdog(Integer value) { this.getWatchdogChannel().setNextValue(value); } - + /** * sets the watchdog. + * * @param value the value * @throws OpenemsNamedException the exception */ public default void setWatchdog(Integer value) throws OpenemsNamedException { this.getWatchdogChannel().setNextWriteValue(value); } - + /** * Gets the Channel for {@link ChannelId#MAX_START_ATTEMPTS}. * @@ -520,7 +544,6 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId SET_SOC(Doc.of(OpenemsType.INTEGER) // .unit(Unit.PERCENT) // .accessMode(AccessMode.WRITE_ONLY)), // - // EnumReadChannels STATE_MACHINE(Doc.of(State.values()) // @@ -569,10 +592,6 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId .unit(Unit.MILLIAMPERE)), // NUMBER_OF_TEMPERATURE_WHEN_ALARM(Doc.of(OpenemsType.INTEGER) // .unit(Unit.NONE)), // - SYSTEM_MAX_CHARGE_CURRENT(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIAMPERE)), // - SYSTEM_MAX_DISCHARGE_CURRENT(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIAMPERE)), // CYCLE_TIME(Doc.of(OpenemsType.INTEGER) // .unit(Unit.NONE)), // TOTAL_CAPACITY_HIGH_BITS(Doc.of(OpenemsType.INTEGER) // @@ -580,7 +599,8 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId TOTAL_CAPACITY_LOW_BITS(Doc.of(OpenemsType.INTEGER) // .unit(Unit.NONE)), // ALARM_FLAG_REGISTER_1(Doc.of(OpenemsType.INTEGER)), // - ALARM_FLAG_REGISTER_2(Doc.of(OpenemsType.INTEGER)), PROTECT_FLAG_REGISTER_1(Doc.of(OpenemsType.INTEGER)), + ALARM_FLAG_REGISTER_2(Doc.of(OpenemsType.INTEGER)), // + PROTECT_FLAG_REGISTER_1(Doc.of(OpenemsType.INTEGER)), // TESTING_IO(Doc.of(OpenemsType.INTEGER)), // SOFT_SHUTDOWN(Doc.of(OpenemsType.INTEGER)), // CURRENT_BOX_SELF_CALIBRATION(Doc.of(OpenemsType.INTEGER)), // @@ -588,7 +608,8 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId INSULATION_SENSOR_FUNCTION(Doc.of(OpenemsType.INTEGER)), // TRANSPARENT_MASTER(Doc.of(OpenemsType.INTEGER)), // SET_EMS_ADDRESS(Doc.of(OpenemsType.INTEGER)), // - SLEEP(Doc.of(OpenemsType.INTEGER).accessMode(AccessMode.READ_WRITE)), //), // + SLEEP(Doc.of(OpenemsType.INTEGER) // + .accessMode(AccessMode.READ_WRITE)), // VOLTAGE_LOW_PROTECTION(Doc.of(OpenemsType.INTEGER) // .unit(Unit.MILLIVOLT)), // WORK_PARAMETER_CURRENT_FIX_COEFFICIENT(Doc.of(OpenemsType.INTEGER)), // diff --git a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionb/SingleRackVersionBImpl.java b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionb/SingleRackVersionBImpl.java index 4420658d21c..4c948f23c8f 100644 --- a/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionb/SingleRackVersionBImpl.java +++ b/io.openems.edge.battery.soltaro/src/io/openems/edge/battery/soltaro/single/versionb/SingleRackVersionBImpl.java @@ -28,11 +28,10 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; import io.openems.edge.battery.api.Battery; -import io.openems.edge.battery.api.SetAllowedCurrents; +import io.openems.edge.battery.protection.BatteryProtection; +import io.openems.edge.battery.soltaro.BatteryProtectionDefinitionSoltaro; import io.openems.edge.battery.soltaro.ChannelIdImpl; import io.openems.edge.battery.soltaro.ModuleParameters; -import io.openems.edge.battery.soltaro.SoltaroCellCharacteristic; -import io.openems.edge.battery.soltaro.single.SingleRackSettings; import io.openems.edge.battery.soltaro.single.versionb.statemachine.Context; import io.openems.edge.battery.soltaro.single.versionb.statemachine.ControlAndLogic; import io.openems.edge.battery.soltaro.single.versionb.statemachine.StateMachine; @@ -53,6 +52,7 @@ import io.openems.edge.common.channel.Channel; import io.openems.edge.common.channel.IntegerDoc; import io.openems.edge.common.channel.IntegerReadChannel; +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.modbusslave.ModbusSlave; @@ -80,6 +80,9 @@ public class SingleRackVersionBImpl extends AbstractOpenemsModbusComponent @Reference protected ConfigurationAdmin cm; + @Reference + protected ComponentManager componentManager; + private AtomicReference startStopTarget = new AtomicReference(StartStop.UNDEFINED); /** @@ -91,24 +94,16 @@ public class SingleRackVersionBImpl extends AbstractOpenemsModbusComponent private Config config; private Map> channelMap; - - private final SetAllowedCurrents setAllowedCurrents; + private BatteryProtection batteryProtection = null; public SingleRackVersionBImpl() { super(// OpenemsComponent.ChannelId.values(), // Battery.ChannelId.values(), // StartStoppable.ChannelId.values(), // - SingleRackVersionB.ChannelId.values() // + SingleRackVersionB.ChannelId.values(), // + BatteryProtection.ChannelId.values() // ); - - this.setAllowedCurrents = new SetAllowedCurrents(// - this,// - new SoltaroCellCharacteristic(), // - new SingleRackSettings(), - this.channel(SingleRackVersionB.ChannelId.SYSTEM_MAX_CHARGE_CURRENT), // - this.channel(SingleRackVersionB.ChannelId.SYSTEM_MAX_DISCHARGE_CURRENT) // - ); } @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) @@ -117,7 +112,7 @@ protected void setModbus(BridgeModbus modbus) { } @Activate - void activate(ComponentContext context, Config config) throws OpenemsException { + protected void activate(ComponentContext context, Config config) throws OpenemsException { this.config = config; // adds dynamically created channels and save them into a map to access them @@ -129,6 +124,10 @@ void activate(ComponentContext context, Config config) throws OpenemsException { return; } + this.batteryProtection = BatteryProtection.create(this) // + .applyBatteryProtectionDefinition(new BatteryProtectionDefinitionSoltaro(), this.componentManager) // + .build(); + ControlAndLogic.setWatchdog(this, config.watchdog()); ControlAndLogic.setSoCLowAlarm(this, config.SoCLowAlarm()); ControlAndLogic.setCapacity(this, config); @@ -173,9 +172,8 @@ public void handleEvent(Event event) { switch (event.getTopic()) { case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE: - - this.setAllowedCurrents.act(); - + // TODO set soltaro protect/recover registers + this.batteryProtection.apply(); break; case EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE: @@ -184,7 +182,6 @@ public void handleEvent(Event event) { } } - @Override public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { return new ModbusSlaveTable(// @@ -282,19 +279,22 @@ protected ModbusProtocol defineModbusProtocol() throws OpenemsException { ), // EMS timeout --> Watchdog - new FC6WriteRegisterTask(0x201C, + new FC6WriteRegisterTask(0x201C, // m(SingleRackVersionB.ChannelId.EMS_COMMUNICATION_TIMEOUT, new UnsignedWordElement(0x201C)) // ), // Sleep - new FC6WriteRegisterTask(0x201D, m(SingleRackVersionB.ChannelId.SLEEP, new UnsignedWordElement(0x201D)) // + new FC6WriteRegisterTask(0x201D, // + m(SingleRackVersionB.ChannelId.SLEEP, new UnsignedWordElement(0x201D)) // ), // Work parameter - new FC6WriteRegisterTask(0x20C1, m(SingleRackVersionB.ChannelId.WORK_PARAMETER_NUMBER_OF_MODULES, - new UnsignedWordElement(0x20C1)) // - ), - new FC3ReadRegistersTask(0x20C1, Priority.LOW, - m(SingleRackVersionB.ChannelId.WORK_PARAMETER_NUMBER_OF_MODULES, new UnsignedWordElement(0x20C1)) // + new FC6WriteRegisterTask(0x20C1, // + m(SingleRackVersionB.ChannelId.WORK_PARAMETER_NUMBER_OF_MODULES, + new UnsignedWordElement(0x20C1)) // + ), // + new FC3ReadRegistersTask(0x20C1, Priority.LOW, // + m(SingleRackVersionB.ChannelId.WORK_PARAMETER_NUMBER_OF_MODULES, + new UnsignedWordElement(0x20C1)) // ), // Paramaeters for configuring @@ -387,10 +387,12 @@ protected ModbusProtocol defineModbusProtocol() throws OpenemsException { ), // Summary state - new FC3ReadRegistersTask(0x2100, Priority.LOW, m(new UnsignedWordElement(0x2100)) // - .m(SingleRackVersionB.ChannelId.CLUSTER_1_VOLTAGE, ElementToChannelConverter.SCALE_FACTOR_2) // - .m(Battery.ChannelId.VOLTAGE, ElementToChannelConverter.SCALE_FACTOR_MINUS_1) // - .build(), // + new FC3ReadRegistersTask(0x2100, Priority.HIGH, // + m(new UnsignedWordElement(0x2100)) // + .m(SingleRackVersionB.ChannelId.CLUSTER_1_VOLTAGE, + ElementToChannelConverter.SCALE_FACTOR_2) // + .m(Battery.ChannelId.VOLTAGE, ElementToChannelConverter.SCALE_FACTOR_MINUS_1) // + .build(), // m(new SignedWordElement(0x2101)) // .m(SingleRackVersionB.ChannelId.CLUSTER_1_CURRENT, ElementToChannelConverter.SCALE_FACTOR_2) // @@ -519,15 +521,11 @@ protected ModbusProtocol defineModbusProtocol() throws OpenemsException { new UnsignedWordElement(0x2153)), // new DummyRegisterElement(0x2154, 0x215A), // m(SingleRackVersionB.ChannelId.OTHER_ALARM_EQUIPMENT_FAILURE, new UnsignedWordElement(0x215B)), // - new DummyRegisterElement(0x215C, 0x215F) // - ), // - - // Allowed Currents high prioritized because it is necessary to react fast on changes - new FC3ReadRegistersTask(0x2160, Priority.HIGH, // - m(SingleRackVersionB.ChannelId.SYSTEM_MAX_CHARGE_CURRENT, new UnsignedWordElement(0x2160), - ElementToChannelConverter.SCALE_FACTOR_2), // - m(SingleRackVersionB.ChannelId.SYSTEM_MAX_DISCHARGE_CURRENT, new UnsignedWordElement(0x2161), - ElementToChannelConverter.SCALE_FACTOR_2) // + new DummyRegisterElement(0x215C, 0x215F), // + m(BatteryProtection.ChannelId.BP_CHARGE_BMS, new UnsignedWordElement(0x2160), + ElementToChannelConverter.SCALE_FACTOR_MINUS_1), // + m(BatteryProtection.ChannelId.BP_DISCHARGE_BMS, new UnsignedWordElement(0x2161), + ElementToChannelConverter.SCALE_FACTOR_MINUS_1) // ), // Cluster info new FC3ReadRegistersTask(0x2180, Priority.LOW, // @@ -634,7 +632,9 @@ protected ModbusProtocol defineModbusProtocol() throws OpenemsException { m(SingleRackVersionB.ChannelId.SLAVE_TEMPERATURE_COMMUNICATION_ERROR_LOW, new UnsignedWordElement(0x21B5)) // ), // + // Add tasks to read/write work and warn parameters + // Stop parameter new FC16WriteRegistersTask(0x2040, // m(SingleRackVersionB.ChannelId.STOP_PARAMETER_CELL_OVER_VOLTAGE_PROTECTION, @@ -777,10 +777,11 @@ protected ModbusProtocol defineModbusProtocol() throws OpenemsException { m(SingleRackVersionB.ChannelId.WARN_PARAMETER_TEMPERATURE_DIFFERENCE_ALARM_RECOVER, new SignedWordElement(0x20A2)) // ), - - new FC6WriteRegisterTask(0x20DF, m(SingleRackVersionB.ChannelId.SET_SOC, new UnsignedWordElement(0x20DF))) - - ); + + new FC6WriteRegisterTask(0x20DF, + m(SingleRackVersionB.ChannelId.SET_SOC, new UnsignedWordElement(0x20DF))) + + ); if (!this.config.ReduceTasks()) { // Stop parameter diff --git a/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versionb/SingleRackVersionBImplTest.java b/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versionb/SingleRackVersionBImplTest.java index 475dae3ce5c..d42ef81e78b 100644 --- a/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versionb/SingleRackVersionBImplTest.java +++ b/io.openems.edge.battery.soltaro/test/io/openems/edge/battery/soltaro/single/versionb/SingleRackVersionBImplTest.java @@ -2,29 +2,23 @@ import org.junit.Test; -import io.openems.common.types.ChannelAddress; import io.openems.edge.battery.soltaro.ModuleType; import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.startstop.StartStopConfig; -import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; +import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; public class SingleRackVersionBImplTest { private static final String BATTERY_ID = "battery0"; private static final String MODBUS_ID = "modbus0"; - private ChannelAddress minCellVoltageAdress = new ChannelAddress(BATTERY_ID, "MinCellVoltage"); - private ChannelAddress maxCellVoltageAdress = new ChannelAddress(BATTERY_ID, "MaxCellVoltage"); - private ChannelAddress capacityAdress = new ChannelAddress(BATTERY_ID, "Capacity"); - private ChannelAddress voltageAdress = new ChannelAddress(BATTERY_ID, "Voltage"); - private ChannelAddress maxDischargeCurrentAdress = new ChannelAddress(BATTERY_ID, "DischargeMaxCurrent"); - @Test public void test() throws Exception { new ComponentTest(new SingleRackVersionBImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // .activate(MyConfig.create() // .setId(BATTERY_ID) // @@ -43,20 +37,6 @@ public void test() throws Exception { .setSoCLowAlarm(0) // .setReduceTasks(false) // .build()) // - - - .next(new TestCase("Empty case")) - - /* set min cell voltage below limit, capacity and voltage are needed to calculate charge current */ - .next(new TestCase("test charge necessary") - .input(this.minCellVoltageAdress, 2750) - .input(this.maxCellVoltageAdress, 3050) - .input(this.capacityAdress, 70000) - .input(this.voltageAdress, 600) - ) - .next(new TestCase().output(this.maxDischargeCurrentAdress, -2)) - - ; } diff --git a/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/KacoBlueplanetGridsaveImpl.java b/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/KacoBlueplanetGridsaveImpl.java index 5c5561b935f..683b841489d 100644 --- a/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/KacoBlueplanetGridsaveImpl.java +++ b/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/src/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/KacoBlueplanetGridsaveImpl.java @@ -394,7 +394,7 @@ public String debugLog() { "|" + this.getCurrentState().asCamelCase(); } - private AtomicReference startStopTarget = new AtomicReference(StartStop.UNDEFINED); + private final AtomicReference startStopTarget = new AtomicReference(StartStop.UNDEFINED); @Override public void setStartStop(StartStop value) { diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/SunSpecModel.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/SunSpecModel.java index 8dea20b6561..54575eaf49c 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/SunSpecModel.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/SunSpecModel.java @@ -8,8 +8,8 @@ public interface SunSpecModel { /** * The name of the SunSpec Model. * - * It is expected to be "S_", e.g. for the common Block-ID "1" the - * expected name is "S_1". + * It is expected to be "S_<Block-ID>", e.g. for the common Block-ID "1" + * the expected name is "S_1". * * @return the name as String */ diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/OneWireAccessProvider.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/OneWireAccessProvider.java index 6b89f0ca6c9..2d94fb4c8e2 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/OneWireAccessProvider.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/OneWireAccessProvider.java @@ -399,8 +399,8 @@ public static DSPortAdapter getAdapter(String adapterName, String portName) *
      *
    • Use adapter/port in System.properties for onewire.adapter.default, and * onewire.port.default properties tags.
    • - *
    • Use adapter/port from onewire.properties file in current directory or < - * java.home >/lib/ (Desktop) or /etc/ (TINI)
    • + *
    • Use adapter/port from onewire.properties file in current directory or + * < java.home >/lib/ (Desktop) or /etc/ (TINI)
    • *
    • Use smart default *
        *
      • Desktop @@ -431,8 +431,8 @@ public static DSPortAdapter getDefaultAdapter() throws OneWireIOException, OneWi *

        *

          *
        • In System.properties - *
        • In onewire.properties file in current directory or < java.home >/lib/ - * (Desktop) or /etc/ (TINI) + *
        • In onewire.properties file in current directory or < java.home + * >/lib/ (Desktop) or /etc/ (TINI) *
        • 'smart' default if property is 'onewire.adapter.default' or * 'onewire.port.default' *
        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 255f1ebd636..19b48462f11 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 @@ -377,7 +377,6 @@ public OWFile(OneWireContainer owd, String parent, String child) { * abstract pathname and the child abstract pathname is resolved against the * parent. * - * @param owd OneWireContainer that this Filesystem resides on * @param parent The parent abstract pathname * @param child The child pathname string * @throws NullPointerException If child is null @@ -957,8 +956,7 @@ public int compareTo(OWFile pathname) { * value greater than zero if this abstract pathname is * lexicographically greater than the argument * - * @throws ClassCastException if the argument is not an abstract - * pathname + * @throws ClassCastException if the argument is not an abstract pathname * * @see java.lang.Comparable */ @@ -995,8 +993,8 @@ public boolean equals(Object obj) { * codes. On UNIX systems, the hash code of an abstract pathname is equal to the * exclusive or of its pathname string and the decimal value * 1234321. On Win32 systems, the hash code is equal to the - * exclusive or of its pathname string, converted to lower case, and the - * decimal value 1234321. + * exclusive or of its pathname string, converted to lower case, and + * the decimal value 1234321. * * @return A hash code for this abstract pathname */ diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileDescriptor.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileDescriptor.java index 60aad403a79..6281fdc25de 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileDescriptor.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/file/OWFileDescriptor.java @@ -504,9 +504,9 @@ public void sync() throws OWSyncFailedException { *
      • filePosition - (file only) overall file position when reading *
      * - * @throws FileNotFoundException when the file/directory path is invalid or - * there was an IOException thrown when trying to - * read the device. + * @throws OWFileNotFoundException when the file/directory path is invalid or + * there was an IOException thrown when trying + * to read the device. */ protected void open() throws OWFileNotFoundException { String last_error = null; @@ -600,18 +600,18 @@ protected void close() throws IOException { /** * Creates a directory or file to write. * - * @param append for files only, true to append data to end of file, false - * to reset the file - * @param isDirectory true if creating a directory, false for a file - * @param makeParents true if creating all needed parent directories in order - * to create the file/directory - * @param startPageNum starting page of file/directory, -1 if not renaming - * @param numberPages number of pages in file/directory, -1 if not renaming - * - * @throws FileNotFoundException if file already opened to write, if - * makeParents=false and parent directories not - * found, if file is read only, or if there is an - * IO error reading filesystem + * @param append for files only, true to append data to end of file, false + * to reset the file + * @param isDirectory true if creating a directory, false for a file + * @param makeParents true if creating all needed parent directories in order to + * create the file/directory + * @param startPage starting page of file/directory, -1 if not renaming + * @param numberPages number of pages in file/directory, -1 if not renaming + * + * @throws OWFileNotFoundException if file already opened to write, if + * makeParents=false and parent directories not + * found, if file is read only, or if there is + * an IO error reading filesystem */ protected void create(boolean append, boolean isDirectory, boolean makeParents, int startPage, int numberPages) throws OWFileNotFoundException { @@ -783,7 +783,8 @@ protected void create(boolean append, boolean isDirectory, boolean makeParents, * WARNING: all files/directories will be deleted in the process. * * @throws OneWireException when adapter is not setup properly - * @throws OneWireIOException when an IO error occurred reading the 1-Wire device + * @throws OneWireIOException when an IO error occurred reading the 1-Wire + * device */ protected void format() throws OneWireException, OneWireIOException { int i, j, len, next_page, cnt, cdcnt = 0, device_map_pages, dm_bytes = 0; @@ -2853,17 +2854,17 @@ private void validateFileSystem() throws OneWireException { /** * Verify the Device Map of a MASTER device is correct. * - * @param page starting page number of the device map file + * @param startPage starting page number of the device map file * @param numberOfContainers to re-create the OneWireContainer array in the * instance variable from the devices listed in the * device map 'owd[]'. Zero indicates leave the list - * alone. >0 means recreate the array keeping the same - * MASTER device. + * alone. >0 means recreate the array keeping the + * same MASTER device. * @param setOverdrive true if set new containers to do a * max speed of overdrive if possible * - * @returns the number of devices in the device map if the current device list - * is INVALID and returns zero if the current device list is VALID. + * @return the number of devices in the device map if the current device list is + * INVALID and returns zero if the current device list is VALID. * * @throws OneWireException when an IO error occurs */ @@ -3116,8 +3117,8 @@ protected boolean createNewFile() throws IOException { * codes. On UNIX systems, the hash code of an abstract pathname is equal to the * exclusive or of its pathname string and the decimal value * 1234321. On Win32 systems, the hash code is equal to the - * exclusive or of its pathname string, converted to lower case, and the - * decimal value 1234321. + * exclusive or of its pathname string, converted to lower case, and + * the decimal value 1234321. * * @return A hash code for this abstract pathname */ 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 f0fb2aa7fca..b7ff339cfd3 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 @@ -28,7 +28,6 @@ package com.dalsemi.onewire.application.file; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -61,8 +60,6 @@ *
    * *

    Usage

    - *
    - *
    *

    Example

    Read from a 1-Wire file on device 'owd': * *
    @@ -116,9 +113,9 @@ public class OWFileInputStream extends InputStream {
     	 *
     	 * @param owd  OneWireContainer that this Filesystem resides on
     	 * @param name the system-dependent file name.
    -	 * @exception FileNotFoundException if the file does not exist, is a directory
    -	 *                                  rather than a regular file, or for some
    -	 *                                  other reason cannot be opened for reading.
    +	 * @exception OWFileNotFoundException if the file does not exist, is a directory
    +	 *                                    rather than a regular file, or for some
    +	 *                                    other reason cannot be opened for reading.
     	 */
     	public OWFileInputStream(OneWireContainer owd, String name) throws OWFileNotFoundException {
     		fd = new OWFileDescriptor(owd, name);
    @@ -155,9 +152,9 @@ public OWFileInputStream(OneWireContainer owd, String name) throws OWFileNotFoun
     	 *
     	 * @param owd  array of OneWireContainers that this Filesystem resides on
     	 * @param name the system-dependent file name.
    -	 * @exception FileNotFoundException if the file does not exist, is a directory
    -	 *                                  rather than a regular file, or for some
    -	 *                                  other reason cannot be opened for reading.
    +	 * @exception OWFileNotFoundException if the file does not exist, is a directory
    +	 *                                    rather than a regular file, or for some
    +	 *                                    other reason cannot be opened for reading.
     	 */
     	public OWFileInputStream(OneWireContainer[] owd, String name) throws OWFileNotFoundException {
     		fd = new OWFileDescriptor(owd, name);
    @@ -190,9 +187,9 @@ public OWFileInputStream(OneWireContainer[] owd, String name) throws OWFileNotFo
     	 * FileNotFoundException is thrown.
     	 *
     	 * @param file the file to be opened for reading.
    -	 * @exception FileNotFoundException if the file does not exist, is a directory
    -	 *                                  rather than a regular file, or for some
    -	 *                                  other reason cannot be opened for reading.
    +	 * @exception OWFileNotFoundException if the file does not exist, is a directory
    +	 *                                    rather than a regular file, or for some
    +	 *                                    other reason cannot be opened for reading.
     	 * @see com.dalsemi.onewire.application.file.OWFile#getPath()
     	 */
     	public OWFileInputStream(OWFile file) throws OWFileNotFoundException {
    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 204ddef097b..61385a67e50 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
    @@ -28,7 +28,6 @@
     
     package com.dalsemi.onewire.application.file;
     
    -import java.io.FileNotFoundException;
     import java.io.IOException;
     import java.io.OutputStream;
     
    @@ -80,8 +79,6 @@
      * 
      *
      * 

    Usage

    - *
    - *
    *

    Example

    Write to a 1-Wire file on device 'owd': * *
    @@ -152,13 +149,13 @@ public class OWFileOutputStream extends OutputStream {
     	 *
     	 * @param owd  OneWireContainer that this Filesystem resides on
     	 * @param name the system-dependent filename
    -	 * @exception FileNotFoundException if the file exists but is a directory rather
    -	 *                                  than a regular file, does not exist but
    -	 *                                  cannot be created, or cannot be opened for
    -	 *                                  any other reason
    -	 * @exception SecurityException     if a security manager exists and its
    -	 *                                  checkWrite method denies write
    -	 *                                  access to the file.
    +	 * @exception OWFileNotFoundException if the file exists but is a directory
    +	 *                                    rather than a regular file, does not exist
    +	 *                                    but cannot be created, or cannot be opened
    +	 *                                    for any other reason
    +	 * @exception SecurityException       if a security manager exists and its
    +	 *                                    checkWrite method denies
    +	 *                                    write access to the file.
     	 */
     	public OWFileOutputStream(OneWireContainer owd, String name) throws OWFileNotFoundException {
     		OneWireContainer[] devices = new OneWireContainer[1];
    @@ -188,13 +185,10 @@ public OWFileOutputStream(OneWireContainer owd, String name) throws OWFileNotFou
     	 *
     	 * @param owd  array of OneWireContainers that this Filesystem resides on
     	 * @param name the system-dependent filename
    -	 * @exception FileNotFoundException if the file exists but is a directory rather
    -	 *                                  than a regular file, does not exist but
    -	 *                                  cannot be created, or cannot be opened for
    -	 *                                  any other reason
    -	 * @exception SecurityException     if a security manager exists and its
    -	 *                                  checkWrite method denies write
    -	 *                                  access to the file.
    +	 * @exception OWFileNotFoundException if the file exists but is a directory
    +	 *                                    rather than a regular file, does not exist
    +	 *                                    but cannot be created, or cannot be opened
    +	 *                                    for any other reason
     	 */
     	public OWFileOutputStream(OneWireContainer[] owd, String name) throws OWFileNotFoundException {
     		fd = new OWFileDescriptor(owd, name);
    @@ -226,13 +220,13 @@ public OWFileOutputStream(OneWireContainer[] owd, String name) throws OWFileNotF
     	 * @param name   the system-dependent file name
     	 * @param append if true, then bytes will be written to the end of
     	 *               the file rather than the beginning
    -	 * @exception FileNotFoundException if the file exists but is a directory rather
    -	 *                                  than a regular file, does not exist but
    -	 *                                  cannot be created, or cannot be opened for
    -	 *                                  any other reason.
    -	 * @exception SecurityException     if a security manager exists and its
    -	 *                                  checkWrite method denies write
    -	 *                                  access to the file.
    +	 * @exception OWFileNotFoundException if the file exists but is a directory
    +	 *                                    rather than a regular file, does not exist
    +	 *                                    but cannot be created, or cannot be opened
    +	 *                                    for any other reason.
    +	 * @exception SecurityException       if a security manager exists and its
    +	 *                                    checkWrite method denies
    +	 *                                    write access to the file.
     	 */
     	public OWFileOutputStream(OneWireContainer owd, String name, boolean append) throws OWFileNotFoundException {
     		fd = new OWFileDescriptor(owd, name);
    @@ -264,13 +258,13 @@ public OWFileOutputStream(OneWireContainer owd, String name, boolean append) thr
     	 * @param name   the system-dependent file name
     	 * @param append if true, then bytes will be written to the end of
     	 *               the file rather than the beginning
    -	 * @exception FileNotFoundException if the file exists but is a directory rather
    -	 *                                  than a regular file, does not exist but
    -	 *                                  cannot be created, or cannot be opened for
    -	 *                                  any other reason.
    -	 * @exception SecurityException     if a security manager exists and its
    -	 *                                  checkWrite method denies write
    -	 *                                  access to the file.
    +	 * @exception OWFileNotFoundException if the file exists but is a directory
    +	 *                                    rather than a regular file, does not exist
    +	 *                                    but cannot be created, or cannot be opened
    +	 *                                    for any other reason.
    +	 * @exception SecurityException       if a security manager exists and its
    +	 *                                    checkWrite method denies
    +	 *                                    write access to the file.
     	 */
     	public OWFileOutputStream(OneWireContainer[] owd, String name, boolean append) throws OWFileNotFoundException {
     		fd = new OWFileDescriptor(owd, name);
    @@ -298,13 +292,13 @@ public OWFileOutputStream(OneWireContainer[] owd, String name, boolean append) t
     	 * FileNotFoundException is thrown.
     	 *
     	 * @param file the file to be opened for writing.
    -	 * @exception FileNotFoundException if the file exists but is a directory rather
    -	 *                                  than a regular file, does not exist but
    -	 *                                  cannot be created, or cannot be opened for
    -	 *                                  any other reason
    -	 * @exception SecurityException     if a security manager exists and its
    -	 *                                  checkWrite method denies write
    -	 *                                  access to the file.
    +	 * @exception OWFileNotFoundException if the file exists but is a directory
    +	 *                                    rather than a regular file, does not exist
    +	 *                                    but cannot be created, or cannot be opened
    +	 *                                    for any other reason
    +	 * @exception SecurityException       if a security manager exists and its
    +	 *                                    checkWrite method denies
    +	 *                                    write access to the file.
     	 * @see java.io.File#getPath()
     	 */
     	public OWFileOutputStream(OWFile file) throws OWFileNotFoundException {
    diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/AbstractDeviceMonitor.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/AbstractDeviceMonitor.java
    index 62e86572948..2e75cc1cc84 100644
    --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/AbstractDeviceMonitor.java
    +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/AbstractDeviceMonitor.java
    @@ -66,18 +66,13 @@
      * environment. Instead of reporting the exception on each failed search
      * attempt, the monitor will default to retrying the search a handful of times
      * before finally reporting the exception.
    - * 
    - * @see #getMaxErrorCount(). To disable this feature, set the max error count to
    - *      1.
    - * @see #setMaxErrorCount(int).
    - *      

    + *

    * - *

    - * To receive events, an object must implement the - * DeviceMonitorEventListener interface. - * @see DeviceMonitorEventListener. And the object must be added to the list of - * listeners. @see #addDeviceMonitorEventListener. - *

    + *

    + * To receive events, an object must implement the + * DeviceMonitorEventListener interface. And the object must be + * added to the list of listeners. @see #addDeviceMonitorEventListener. + *

    * * @author SH * @version 1.00 diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/ChainMonitor.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/ChainMonitor.java index 25b5e511917..8a8fe95e3d7 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/ChainMonitor.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/ChainMonitor.java @@ -58,7 +58,7 @@ public class ChainMonitor extends AbstractDeviceMonitor { /** * Create a simple monitor that does not search branches * - * @param the DSPortAdapter this monitor should search + * @param adapter the DSPortAdapter this monitor should search */ public ChainMonitor(DSPortAdapter adapter) { setAdapter(adapter); @@ -67,7 +67,7 @@ public ChainMonitor(DSPortAdapter adapter) { /** * Sets this monitor to search a new DSPortAdapter * - * @param the DSPortAdapter this monitor should search + * @param adapter the DSPortAdapter this monitor should search */ public void setAdapter(DSPortAdapter adapter) { if (adapter == null) @@ -94,7 +94,6 @@ public OWPath getDevicePath(Long address) { /** * chainOn sends the chain mode "ON" command sequence to all chain devices. * - * @param none * @return true if successful, false otherwise */ public boolean chainOn() throws OneWireException, OneWireIOException { @@ -128,7 +127,7 @@ public boolean chainOn() throws OneWireException, OneWireIOException { * chainConditionalReadRom sends the chain mode "DONE" command sequence to * current chain device. * - * @param 8-byte array for chain 1-Wire net address + * @param chainDeviceAddress 8-byte array for chain 1-Wire net address * @return true if successful, false otherwise */ public boolean chainConditionalReadRom(byte[] chainDeviceAddress) throws OneWireException, OneWireIOException { @@ -170,7 +169,6 @@ public boolean chainConditionalReadRom(byte[] chainDeviceAddress) throws OneWire * chainDone sends the chain mode "DONE" command sequence to current chain * device. * - * @param none * @return true if successful, false otherwise */ public boolean chainDone() throws OneWireException, OneWireIOException { @@ -203,7 +201,6 @@ public boolean chainDone() throws OneWireException, OneWireIOException { /** * chainOff sends the chain mode "OFF" command sequence to all chain devices. * - * @param none * @return true if successful, false otherwise */ public boolean chainOff() throws OneWireException, OneWireIOException { diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/DeviceMonitor.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/DeviceMonitor.java index d913cc582e1..7668e2c5844 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/DeviceMonitor.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/DeviceMonitor.java @@ -54,7 +54,7 @@ public class DeviceMonitor extends AbstractDeviceMonitor { /** * Create a simple monitor that does not search branches * - * @param the DSPortAdapter this monitor should search + * @param adapter the DSPortAdapter this monitor should search */ public DeviceMonitor(DSPortAdapter adapter) { setAdapter(adapter); @@ -63,7 +63,7 @@ public DeviceMonitor(DSPortAdapter adapter) { /** * Sets this monitor to search a new DSPortAdapter * - * @param the DSPortAdapter this monitor should search + * @param adapter the DSPortAdapter this monitor should search */ public void setAdapter(DSPortAdapter adapter) { if (adapter == null) @@ -88,9 +88,9 @@ public OWPath getDevicePath(Long address) { } /** - * Sets this monitor to search for alarming parts + * Sets this monitor to search for alarming parts. * - * @param the DSPortAdapter this monitor should search + * @param findAlarmingParts */ public void setDoAlarmSearch(boolean findAlarmingParts) { synchronized (sync_flag) { @@ -99,9 +99,7 @@ public void setDoAlarmSearch(boolean findAlarmingParts) { } /** - * See if Gets this monitor to search for alarming parts - * - * @param the DSPortAdapter this monitor should search + * See if Gets this monitor to search for alarming parts. */ public boolean getDoAlarmSearch() { return doAlarmSearch; diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/NetworkDeviceMonitor.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/NetworkDeviceMonitor.java index 1d17228d0bf..d342dfc253b 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/NetworkDeviceMonitor.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/monitor/NetworkDeviceMonitor.java @@ -57,7 +57,7 @@ public class NetworkDeviceMonitor extends AbstractDeviceMonitor { /** * Create a complex monitor that does search branches * - * @param the DSPortAdapter this monitor should search + * @param adapter the DSPortAdapter this monitor should search */ public NetworkDeviceMonitor(DSPortAdapter adapter) { setAdapter(adapter); @@ -66,7 +66,7 @@ public NetworkDeviceMonitor(DSPortAdapter adapter) { /** * Sets this monitor to search a new DSPortAdapter * - * @param the DSPortAdapter this monitor should search + * @param adapter the DSPortAdapter this monitor should search */ public void setAdapter(DSPortAdapter adapter) { if (adapter == null) @@ -100,8 +100,8 @@ public void setBranchAutoSearching(boolean enabled) { * Indicates whether or not branches are automatically traversed. If false, new * branches must be indicated using the "addBranch" method. * - * @returns true if all branches are automatically traversed during a search - * operation. + * @return true if all branches are automatically traversed during a search + * operation. */ public boolean getBranchAutoSearching() { return this.branchAutoSearching; diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Contact.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Contact.java index 7f6f3dd61fe..89b89762e87 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Contact.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Contact.java @@ -49,8 +49,7 @@ public Contact() { * connected to the supplied port adapter. * * @param adapter The adapter serving the sensor. - * @param NetAddress The 1-Wire network address of the sensor. - * @param netAddress + * @param netAddress The 1-Wire network address of the sensor. */ public Contact(DSPortAdapter adapter, String netAddress) { super(adapter, netAddress); @@ -60,10 +59,7 @@ public Contact(DSPortAdapter adapter, String netAddress) { * The readSensor method returns the "max" string if the Sensor is present or * the "min" string if the Sensor is not present. * - * @param--none. - * - * @return The "max" string if sensor is present or "min" string - * if not. + * @return The "max" string if sensor is present or "min" string if not. */ public String readSensor() throws OneWireException { String returnString = ""; diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/D2A.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/D2A.java index 34120430783..8ba5b16ff4f 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/D2A.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/D2A.java @@ -71,10 +71,8 @@ public Vector getSelections() { /** * Set the selection of this actuator * - * @param The selection string. - * + * @param selection The selection string. * @throws OneWireException - * */ public void setSelection(String selection) throws OneWireException { PotentiometerContainer pc = (PotentiometerContainer) getDeviceContainer(); @@ -109,10 +107,7 @@ public void setSelection(String selection) throws OneWireException { /** * Initializes the actuator * - * @param Init The initialization string. - * * @throws OneWireException - * */ public void initActuator() throws OneWireException { PotentiometerContainer pc = (PotentiometerContainer) getDeviceContainer(); diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Event.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Event.java index 53cde287da9..a9efe42401d 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Event.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Event.java @@ -61,9 +61,7 @@ public Event(DSPortAdapter adapter, String netAddress) { * The readSensor method returns the "max" string if the Sensor (a switch) has * had activity since last time it was checked for activity. * - * @param--none. - * - * @return String The "max" string associated with this Sensor. + * @return String The "max" string associated with this Sensor. */ public String readSensor() throws OneWireException { String returnString = ""; diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Humidity.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Humidity.java index b7e3f36321e..aa94b311bab 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Humidity.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Humidity.java @@ -59,9 +59,7 @@ public Humidity(DSPortAdapter adapter, String netAddress) { /** * The readSensor method returns a relative humidity reading in %RH * - * @param--none. - * - * @return String humidity in %RH + * @return String humidity in %RH */ public String readSensor() throws OneWireException { HumidityContainer hc = (HumidityContainer) DeviceContainer; diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Level.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Level.java index 506896ff5ac..5a995b50718 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Level.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Level.java @@ -58,15 +58,13 @@ public Level(DSPortAdapter adapter, String netAddress) { } /** - * The readSensor method returns the or string of the Sensor (in - * this case, a switch). The elements and represent conducting and + * The readSensor method returns the "max" or "min" string of the Sensor (in + * this case, a switch). The elements "max" and "min" represent conducting and * non-conducting states of the switch, respectively. * - * @param--none. - * - * @return String The string is associated with the - * conducting switch state, and the string is associated - * with the non-conducting state of the 1-Wire switch. + * @return String The "max" string is associated with the conducting switch + * state, and the "min" string is associated with the non-conducting + * state of the 1-Wire switch. */ public String readSensor() throws OneWireException { String returnString = ""; diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/SAXParser.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/SAXParser.java index 3f36c685e3f..1880f43cdd2 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/SAXParser.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/SAXParser.java @@ -166,9 +166,7 @@ public void setErrorHandler(ErrorHandler handler) { /** * Parse an XML document. * - * @param source Source of the document to parse. - * - * @param inputSource + * @param inputSource Source of the document to parse. * @throws SAXException Any SAX exception, possibly wrapping another exception. * @throws IOException If an I/O error occurred while reading the document. */ diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Switch.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Switch.java index ea1c5c9cd59..b8d48a37d60 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Switch.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/Switch.java @@ -71,8 +71,6 @@ public Vector getSelections() { /** * Set the selection of this actuator * - * @param The selection string. - * * @throws OneWireException * */ @@ -105,8 +103,6 @@ public void setSelection(String selection) throws OneWireException { /** * Initializes the actuator * - * @param Init The initialization string. - * * @throws OneWireException * */ diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/TAGParser.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/TAGParser.java index 831b50e0b51..ae44a3719d2 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/TAGParser.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/TAGParser.java @@ -85,8 +85,6 @@ public Vector parse(InputStream in) throws SAXException, IOExcepti * Returns the vector of Branch TaggedDevice objects described in the TAG file. * The XML should already be parsed before calling this method. * - * @param in The XML document to parse. - * * @return Vector of Branch TaggedDevice objects. */ public Vector getBranches() { @@ -100,8 +98,6 @@ public Vector getBranches() { * Returns the vector of OWPath objects discovered through parsing the XML file. * The XML file should already be parsed before calling this method. * - * @param no parameters. - * * @return Vector of OWPath objects. */ public Vector getOWPaths() { diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/TaggedDevice.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/TaggedDevice.java index b8e174b9d42..85111b655a2 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/TaggedDevice.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/application/tag/TaggedDevice.java @@ -44,8 +44,7 @@ public class TaggedDevice { * connected to the supplied port adapter. * * @param adapter The adapter serving the sensor. - * @param NetAddress The 1-Wire network address of the sensor. - * @param netAddress + * @param netAddress The 1-Wire network address of the sensor. */ public TaggedDevice(DSPortAdapter adapter, String netAddress) { this.DeviceContainer = adapter.getDeviceContainer(netAddress); @@ -63,7 +62,7 @@ public TaggedDevice() { * Sets the 1-Wire Container for the tagged device. */ public void setDeviceContainer(DSPortAdapter adapter, String netAddress) { - DeviceContainer = adapter.getDeviceContainer(netAddress); + this.DeviceContainer = adapter.getDeviceContainer(netAddress); } /** @@ -72,7 +71,7 @@ public void setDeviceContainer(DSPortAdapter adapter, String netAddress) { * @param tType */ public void setDeviceType(String tType) { - DeviceType = tType; + this.DeviceType = tType; } /** @@ -81,7 +80,7 @@ public void setDeviceType(String tType) { * @param Label */ public void setLabel(String Label) { - label = Label; + this.label = Label; } /** @@ -90,7 +89,7 @@ public void setLabel(String Label) { * @param Channel */ public void setChannelFromString(String Channel) { - channel = new Integer(Channel); + this.channel = new Integer(Channel); } /** @@ -98,8 +97,8 @@ public void setChannelFromString(String Channel) { * * @param Channel */ - public void setChannel(int Channel) { - channel = new Integer(Channel); + public void setChannel(int channel) { + this.channel = new Integer(channel); } /** @@ -107,8 +106,8 @@ public void setChannel(int Channel) { * * @param init */ - public void setInit(String Init) { - init = Init; + public void setInit(String init) { + this.init = init; } /** @@ -117,7 +116,7 @@ public void setInit(String Init) { * @param cluster */ public void setClusterName(String cluster) { - clusterName = cluster; + this.clusterName = cluster; } /** @@ -126,7 +125,7 @@ public void setClusterName(String cluster) { * @param branches */ public void setBranches(Vector branches) { - branchVector = branches; + this.branchVector = branches; } /** @@ -136,7 +135,7 @@ public void setBranches(Vector branches) { * @param branchOWPath */ public void setOWPath(OWPath branchOWPath) { - branchPath = branchOWPath; + this.branchPath = branchOWPath; } /** @@ -147,14 +146,14 @@ public void setOWPath(OWPath branchOWPath) { * @param Branches */ public void setOWPath(DSPortAdapter adapter, Vector Branches) { - branchPath = new OWPath(adapter); + this.branchPath = new OWPath(adapter); TaggedDevice TDevice; for (int i = 0; i < Branches.size(); i++) { TDevice = (TaggedDevice) Branches.elementAt(i); - branchPath.add(TDevice.getDeviceContainer(), TDevice.getChannel()); + this.branchPath.add(TDevice.getDeviceContainer(), TDevice.getChannel()); } } @@ -166,7 +165,7 @@ public void setOWPath(DSPortAdapter adapter, Vector Branches) { * @return The 1-Wire container for the tagged device. */ public OneWireContainer getDeviceContainer() { - return DeviceContainer; + return this.DeviceContainer; } /** @@ -175,7 +174,7 @@ public OneWireContainer getDeviceContainer() { * @return The device type for the tagged device. */ public String getDeviceType() { - return DeviceType; + return this.DeviceType; } /** @@ -184,7 +183,7 @@ public String getDeviceType() { * @return The label for the tagged device. */ public String getLabel() { - return label; + return this.label; } /** @@ -193,7 +192,7 @@ public String getLabel() { * @return The channel for the tagged device as a String. */ public String getChannelAsString() { - return channel.toString(); + return this.channel.toString(); } /** @@ -202,7 +201,7 @@ public String getChannelAsString() { * @return The channel for the tagged device as an int. */ public int getChannel() { - return channel.intValue(); + return this.channel.intValue(); } /** @@ -211,7 +210,7 @@ public int getChannel() { * @return String init (Initialization String) */ public String getInit() { - return init; + return this.init; } /** @@ -220,7 +219,7 @@ public String getInit() { * @return String Gets the max string */ public String getMax() { - return max; + return this.max; } /** @@ -229,7 +228,7 @@ public String getMax() { * @return String Gets the min string */ public String getMin() { - return min; + return this.min; } /** @@ -238,7 +237,7 @@ public String getMin() { * @return The cluster name for the tagged device. */ public String getClusterName() { - return clusterName; + return this.clusterName; } /** @@ -247,7 +246,7 @@ public String getClusterName() { * @return The vector of branches to get to the tagged device. */ public Vector getBranches() { - return branchVector; + return this.branchVector; } /** @@ -257,7 +256,7 @@ public Vector getBranches() { * @return The OWPath for the tagged device. */ public OWPath getOWPath() { - return branchPath; + return this.branchPath; } public boolean equals(Object o) { @@ -278,7 +277,7 @@ public int hashCode() { } public String toString() { - return getLabel(); + return this.getLabel(); } /** ********* Properties (fields) for this object ********** */ diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/CommandAPDU.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/CommandAPDU.java index 6543b3b052f..0a40644f795 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/CommandAPDU.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/CommandAPDU.java @@ -57,13 +57,16 @@ * *

    Usage

    *
      - *
    1. - *   byte[] buffer = {(byte)0x90, (byte)0x00, (byte)0x00, (byte)0x00, 
      - *                    (byte)0x01, (byte)0x02, (byte)0x03};
      - *   CommandAPDU capdu = new CommandAPDU(buffer); 
      - *
    2. - *   CommandAPDU capdu = new CommandAPDU((byte)0x90, (byte)0x00, (byte)0x00, (byte)0x00, 
      - *                                       (byte)0x01, (byte)0x02, (byte)0x03);
      + * + *
      + * byte[] buffer = { (byte) 0x90, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03 };
      + * CommandAPDU capdu = new CommandAPDU(buffer);
      + * 
      + * + *
      + * CommandAPDU capdu = new CommandAPDU((byte) 0x90, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x02,
      + * 		(byte) 0x03);
      + * 
      *
    * *

    Additional information

    @@ -72,8 +75,6 @@ *
    * * @see com.dalsemi.onewire.container.ResponseAPDU - * @see com.dalsemi.onewire.container.OneWireContainer16 - * * @version 0.00, 28 Aug 2000 * @author YL * diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/HumidityContainer.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/HumidityContainer.java index 226e1159c86..7db95ba9246 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/HumidityContainer.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/HumidityContainer.java @@ -67,8 +67,6 @@ * *

    Usage

    * - *
    - *
    *

    Example

    Gets humidity reading from a HumidityContainer instance * 'hc': * diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBank.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBank.java index 44af2566397..42db04c135c 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBank.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MemoryBank.java @@ -95,13 +95,11 @@ * with zeros: * *
    - *  
    - *  byte[] write_buf = new byte[mb.getSize()];
    - *  for (int i = 0; i < write_buf.length; i++)
    - *      write_buf[i] = (byte)0;
    + * byte[] write_buf = new byte[mb.getSize()];
    + * for (int i = 0; i > write_buf.length; i++)
    + * 	write_buf[i] = (byte) 0;
      * 
    - *  mb.write(0, write_buf, 0, write_buf.length);
    - * 
    + * mb.write(0, write_buf, 0, write_buf.length);
      * 
    * *
    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 42e6fdce04c..30b470dc762 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 @@ -736,11 +736,7 @@ public void loadFirstSecret(int addr) throws OneWireIOException, OneWireExceptio /** * Computes the next secret. * - * @param addr the physical address of the page to use for secret - * computation - * @param partialsecret byte array containing next partial secret for writing to - * the scratchpad - * + * @param addr the physical address of the page to use for secret computation * @throws OneWireIOException * @throws OneWireException */ diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MissionContainer.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MissionContainer.java index 2746de3eae2..09b77a7696c 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MissionContainer.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/MissionContainer.java @@ -184,7 +184,6 @@ void startNewMission(int sampleRate, int missionStartDelay, boolean rolloverEnab * channel's readings will be recorded in the mission log. * * @param channel the channel to enable/disable - * @param enable if true, the channel is enabled */ boolean getMissionChannelEnable(int channel) throws OneWireException, OneWireIOException; diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer.java index 7b9c4d773a2..147937f10ca 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer.java @@ -130,7 +130,7 @@ public class OneWireContainer { *
  • 1 (SPEED_FLEX) *
  • 2 (SPEED_OVERDRIVE) *
  • 3 (SPEED_HYPERDRIVE) - *
  • >3 future speeds + *
  • >3 future speeds * * * @see DSPortAdapter#setSpeed @@ -357,7 +357,7 @@ public String getDescription() { * overdrive *
  • 3 (SPEED_HYPERDRIVE) set to normal communication speed to * hyperdrive - *
  • >3 future speeds + *
  • >3 future speeds * * * @param fallBack boolean indicating it is OK to fall back to a slower speed if diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer01.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer01.java index 15ae4ec70fa..eb03a6ff262 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer01.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer01.java @@ -45,8 +45,7 @@ *

    Features

    *
      *
    • 64 bit unique serial number - *
    • Operating temperature range from -40@htmlonly °C @endhtmlonly to - * +85@htmlonly °C @endhtmlonly + *
    • Operating temperature range from -40 to +85 *
    * *

    Alternate Names

    diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer02.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer02.java index 42152f23c81..3b16fd9ec6e 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer02.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer02.java @@ -51,8 +51,7 @@ *
  • 64 bit (8 byte) password per memory block *
  • 64 bit (8 byte) identification per memory block *
  • Data integrity assured with strict read/write protocols - *
  • Operating temperature range from -40@htmlonly °C @endhtmlonly to - * +70@htmlonly °C @endhtmlonly + *
  • Operating temperature range from -40 to +70 *
  • Over 10 years of data retention * * diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer04.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer04.java index 412dd994c6e..7bb9ea30935 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer04.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer04.java @@ -67,8 +67,7 @@ * timekeeping *
  • Clock accuracy is better than @htmlonly ± @endhtmlonly 2 minute/ * month at 25@htmlonly °C @endhtmlonly - *
  • Operating temperature range from -40@htmlonly °C @endhtmlonly to - * +70@htmlonly °C @endhtmlonly + *
  • Operating temperature range from -40 to +70 *
  • Over 10 years of data retention * * @@ -732,8 +731,8 @@ public boolean isClockRunning(byte[] state) { * @param state current state of the device returned from * readDevice() * - * @return time in milliseconds that have occurred since the interval counter was - * started + * @return time in milliseconds that have occurred since the interval counter + * was started * * @see com.dalsemi.onewire.container.OneWireSensor#readDevice() * @see #setIntervalTimer(long,byte[]) diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer05.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer05.java index 4e4ca37b60a..8ceb87bce0b 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer05.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer05.java @@ -43,8 +43,7 @@ *
      *
    • Open drain PIO pin controlled through 1-Wire communication *
    • Logic level sensing of the PIO pin can be sensed - *
    • Operating temperature range from -40@htmlonly °C @endhtmlonly to - * +85@htmlonly °C @endhtmlonly + *
    • Operating temperature range from -40 to +85 *
    • One channel with level sensing abilities *
    • Does not support activity sensing or 'Smart On' capabilities *
    @@ -64,36 +63,35 @@ * level of the PIO pin, then in the loop it toggles the latch state. *

    * - *
    - *      // "ID" is a byte array of size 8 with an address of a part we
    - *      // have already found with family code 05 hex
    - *      // "access" is a DSPortAdapter
    + * 
    + * // "ID" is a byte array of size 8 with an address of a part we
    + * // have already found with family code 05 hex
    + * // "access" is a DSPortAdapter
      *
    - *      int i=0;
    - *      OneWireContainer05 ds2405 = (OneWireContainer05) access.getDeviceContainer(ID);
    - *      ds2405.setupContainer(access,ID);
    + * int i = 0;
    + * OneWireContainer05 ds2405 = (OneWireContainer05) access.getDeviceContainer(ID);
    + * ds2405.setupContainer(access, ID);
      *
    - *      byte[] state = ds2405.readDevice();
    + * byte[] state = ds2405.readDevice();
      *
    - *      // I know that the 2405 only has one channel (one switch)
    - *      // and it doesn't support 'Smart On'
    + * // I know that the 2405 only has one channel (one switch)
    + * // and it doesn't support 'Smart On'
      *
    - *      boolean latch_state = ds2405.getLatchState(0,state);
    - *      System.out.println("Current state of switch: "+latch_state);
    - *      System.out.println("Current output level:    "+ds2405.getLevel(0,state));
    - *      while (++i < 100)
    - *      {
    - *          System.out.println("Toggling switch");
    - *          ds2405.setLatchState(0,!latch_state,false,state);
    - *          ds2405.writeDevice(state);
    - *          state = ds2405.readDevice();
    - *          latch_state = ds2405.getLatchState(0,state);
    - *          System.out.println("Current state of switch: "+latch_state);
    - *          System.out.println("Current output level:    "+ds2405.getLevel(0,state));
    - *          Thread.sleep(500);
    - *      }
    + * boolean latch_state = ds2405.getLatchState(0, state);
    + * System.out.println("Current state of switch: " + latch_state);
    + * System.out.println("Current output level:    " + ds2405.getLevel(0, state));
    + * while (++i < 100) {
    + * 	System.out.println("Toggling switch");
    + * 	ds2405.setLatchState(0, !latch_state, false, state);
    + * 	ds2405.writeDevice(state);
    + * 	state = ds2405.readDevice();
    + * 	latch_state = ds2405.getLatchState(0, state);
    + * 	System.out.println("Current state of switch: " + latch_state);
    + * 	System.out.println("Current output level:    " + ds2405.getLevel(0, state));
    + * 	Thread.sleep(500);
    + * }
      *
    - * 
    + *
    * *

    * Also see the usage example in the diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer06.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer06.java index 7a629ce1b1b..72b69a9c10f 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer06.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer06.java @@ -50,8 +50,7 @@ *

  • 256-bit (32-byte) scratchpad ensures integrity of data transfer *
  • Memory partitioned into 256-bit (32-byte) pages for packetizing data *
  • Data integrity assured with strict read/write protocols - *
  • Operating temperature range from -40@htmlonly °C @endhtmlonly to - * +70@htmlonly °C @endhtmlonly + *
  • Operating temperature range from -40 to +70 *
  • Over 10 years of data retention * * diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer08.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer08.java index 850608ebcab..6c6558a4e56 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer08.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer08.java @@ -50,8 +50,7 @@ *
  • 256-bit (32-byte) scratchpad ensures integrity of data transfer *
  • Memory partitioned into 256-bit (32-byte) pages for packetizing data *
  • Data integrity assured with strict read/write protocols - *
  • Operating temperature range from -40@htmlonly °C @endhtmlonly to - * +70@htmlonly °C @endhtmlonly + *
  • Operating temperature range from -40 to +70 *
  • Over 10 years of data retention * * diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer18.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer18.java index 52aaae40df2..4232277107b 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer18.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer18.java @@ -102,7 +102,6 @@ * readAuthenticatedPage} *
  • {@link #writeDataPage(int,byte[]) writeDataPage} * - *

    * *

    * The memory can also be accessed through the objects that are returned from @@ -797,55 +796,56 @@ public synchronized boolean eraseScratchPad(int page) throws OneWireIOException, * the DS1963S to stop sending 0x0ff's and begin alternating its output bits. * This method reads until it finds a non-0x0ff byte or until it * reaches a specified number of tries, indicating failure. - *

    * *

    * This method can often be optimized away. A normal 1-Wire transaction involves * writing and reading a known number of bytes. If a few more bytes are read, a * program can check to see if the DS1963S has started alternating its output * much quicker than calling this method will. For instance, to copy the - * scratchpad, the source code might look like this:

    -	 *    buffer [0] = COPY_SCRATCHPAD;
    -	 *    buffer [1] = TA1;
    -	 *    buffer [2] = TA2;
    -	 *    buffer [3] = ES;
    -	 *
    -	 *    adapter.dataBlock(buffer,0,4);
    -	 *    return waitForSuccessfulFinish();
    -	 * 
    To optimize the code, read more bytes than required: - *
    -	 *    buffer [0] = COPY_SCRATCHPAD;
    -	 *    buffer [1] = TA1;
    -	 *    buffer [2] = TA2;
    -	 *    buffer [3] = ES;
    -	 *
    -	 *    //copy 0x0FF into the buffer, this effectively reads
    -	 *    System.arraycopy(FF, 0, buffer, 4, 5);
    -	 *
    -	 *    //read 5 extra bytes
    -	 *    adapter.dataBlock(buffer, 0, 9);
    -	 *
    -	 *    //if the last byte has not shown alternating output,
    -	 *    //still call waitForSuccessfulFinish(), else
    -	 *    //we are already done
    -	 *    if (buffer [8] == ( byte ) 0x0ff)
    -	 *         return waitForSuccessfulFinish();
    -	 *     else
    -	 *         return true;
    -	 * 
    - *

    + * scratchpad, the source code might look like this: + * + *
    +	 * buffer[0] = COPY_SCRATCHPAD;
    +	 * buffer[1] = TA1;
    +	 * buffer[2] = TA2;
    +	 * buffer[3] = ES;
    +	 *
    +	 * adapter.dataBlock(buffer, 0, 4);
    +	 * return waitForSuccessfulFinish();
    +	 * 
    + * + * To optimize the code, read more bytes than required: + * + *
    +	 * buffer[0] = COPY_SCRATCHPAD;
    +	 * buffer[1] = TA1;
    +	 * buffer[2] = TA2;
    +	 * buffer[3] = ES;
    +	 *
    +	 * // copy 0x0FF into the buffer, this effectively reads
    +	 * System.arraycopy(FF, 0, buffer, 4, 5);
    +	 *
    +	 * // read 5 extra bytes
    +	 * adapter.dataBlock(buffer, 0, 9);
    +	 *
    +	 * // if the last byte has not shown alternating output,
    +	 * // still call waitForSuccessfulFinish(), else
    +	 * // we are already done
    +	 * if (buffer[8] == (byte) 0x0ff)
    +	 * 	return waitForSuccessfulFinish();
    +	 * else
    +	 * 	return true;
    +	 * 
    * *

    * The second method is faster because it is more expensive to invoke another * method that goes down to the native access layer than it is to just read a * few more bytes while the program is already at the native access layer. - *

    * *

    * See the datasheet for which operations function in this manner. Only call * this method after another method which has successfully communicated with the * DS1963S. - *

    * * @return true if the DS1963S completed its operation successfully * @@ -1668,7 +1668,6 @@ private synchronized boolean write_read_copy_quick(int secret_page, int secret_o * However, this method makes several optimizations to help it run faster. * Because of the optimizations, this is the preferred way of writing data to a * normal memory page on the DS1963S. - *

    * * @param page_number page number to write * @param page_data page data to write (must be at least 32 bytes long) diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1A.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1A.java index e199895a9d8..0ab175c3ca7 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1A.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1A.java @@ -56,8 +56,7 @@ *
  • Four 32-bit read-only non rolling-over page write cycle counters *
  • 32 factory-preset tamper-detect bits to indicate physical intrusion *
  • On-chip 16-bit CRC generator for safeguarding data transfers - *
  • Operating temperature range from -40@htmlonly °C @endhtmlonly to - * +70@htmlonly °C @endhtmlonly + *
  • Operating temperature range from -40 to +70 *
  • Over 10 years of data retention * * diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1C.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1C.java index aefdfff9bdb..a8a624a8b6b 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1C.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1C.java @@ -709,11 +709,8 @@ public void andConditionalSearch(byte[] register) { * Checks if the 'PIO Level' Conditional Search is set for input and if not sets * it. * - * @param pinActivity if true, the activity latch for the pin is used for the - * conditional search. Otherwise, the sensed level of the pin - * is used for the conditional search. - * @param register current register for conditional search, which if returned - * from readRegister() + * @param register current register for conditional search, which if returned + * from readRegister() */ public void setConditionalSearchLogicLevel(byte[] register) { if ((register[2] & (byte) 0x01) == (byte) 0x01) { diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1D.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1D.java index b1813d7f1f8..64d0e33ac3d 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1D.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/container/OneWireContainer1D.java @@ -69,8 +69,7 @@ * debouncing compatible with reed and Wiegand switches *
  • 32 factory-preset tamper-detect bits to indicate physical intrusion *
  • On-chip 16-bit CRC generator for safeguarding data transfers - *
  • Operating temperature range from -40@htmlonly °C @endhtmlonly to - * +70@htmlonly °C @endhtmlonly + *
  • Operating temperature range from -40 to +70 *
  • Over 10 years of data retention * * diff --git a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/debug/Debug.java b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/debug/Debug.java index d9d04d982ec..d13c32f3fa3 100644 --- a/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/debug/Debug.java +++ b/io.openems.edge.bridge.onewire/src/com/dalsemi/onewire/debug/Debug.java @@ -80,8 +80,8 @@ public class Debug { /** * Sets the debug printing mode for this application. * - * @param true to see debug messages, false to - * suppress them + * @param onoff true to see debug messages, false to + * suppress them */ public static final void setDebugMode(boolean onoff) { DEBUG = onoff; @@ -100,7 +100,7 @@ public static final boolean getDebugMode() { /** * Sets the output stream for printing the debug info. * - * @param out the output stream for printing the debug info. + * @param outStream the output stream for printing the debug info. */ public static final void setPrintStream(PrintStream outStream) { out = outStream; @@ -109,12 +109,18 @@ public static final void setPrintStream(PrintStream outStream) { /** * Prints the specified java.lang.String object if debug mode is * enabled. This method calls PrintStream.println(String), and - * pre-pends the String ">> " to the message, so that if a program - * were to call (when debug mode was enabled):
    -	 *     com.dalsemi.onewire.debug.Debug.debug("Some notification...");
    -	 * 
    the resulting output would look like:
    -	 *     >> Some notification...
    -	 * 
    + * pre-pends the String ">> " to the message, so that if a + * program were to call (when debug mode was enabled): + * + *
    +	 * com.dalsemi.onewire.debug.Debug.debug("Some notification...");
    +	 * 
    + * + * the resulting output would look like: + * + *
    +	 *     >> Some notification...
    +	 * 
    * * @param x the message to print out if in debug mode */ @@ -126,13 +132,19 @@ public static final void debug(String x) { /** * Prints the specified array of bytes with a given label if debug mode is * enabled. This method calls PrintStream.println(String), and - * pre-pends the String ">> " to the message, so that if a program - * were to call (when debug mode was enabled):
    -	 *     com.dalsemi.onewire.debug.Debug.debug("Some notification...", myBytes);
    -	 * 
    the resulting output would look like:
    +	 * pre-pends the String ">> " to the message, so that if a
    +	 * program were to call (when debug mode was enabled):
    +	 * 
    +	 * 
    +	 * com.dalsemi.onewire.debug.Debug.debug("Some notification...", myBytes);
    +	 * 
    + * + * the resulting output would look like: + * + *
     	 *     >> my label
     	 *     >>   FF F1 F2 F3 F4 F5 F6 FF
    -	 * 
    + *
    * * @param lbl the message to print out above the array * @param bytes the byte array to print out @@ -145,13 +157,19 @@ public static final void debug(String lbl, byte[] bytes) { /** * Prints the specified array of bytes with a given label if debug mode is * enabled. This method calls PrintStream.println(String), and - * pre-pends the String ">> " to the message, so that if a program - * were to call (when debug mode was enabled):
    -	 *     com.dalsemi.onewire.debug.Debug.debug("Some notification...", myBytes, 0, 8);
    -	 * 
    the resulting output would look like:
    -	 *     >> my label
    -	 *     >>   FF F1 F2 F3 F4 F5 F6 FF
    -	 * 
    + * pre-pends the String ">> " to the message, so that if a + * program were to call (when debug mode was enabled): + * + *
    +	 * com.dalsemi.onewire.debug.Debug.debug("Some notification...", myBytes, 0, 8);
    +	 * 
    + * + * the resulting output would look like: + * + *
    +	 *     >> my label
    +	 *     >> FF F1 F2 F3 F4 F5 F6 FF
    +	 * 
    * * @param lbl the message to print out above the array * @param bytes the byte array to print out @@ -182,18 +200,16 @@ public static final void debug(String lbl, byte[] bytes, int offset, int length) /** * Prints the specified exception with a given label if debug mode is enabled. * This method calls PrintStream.println(String), and pre-pends the - * String ">> " to the message, so that if a program were to call - * (when debug mode was enabled):
    +	 * String ">>" to the message, so that if a program were to
    +	 * call (when debug mode was enabled): 
     	 *     com.dalsemi.onewire.debug.Debug.debug("Some notification...", exception);
     	 * 
    the resulting output would look like:
    -	 *     >> my label
    -	 *     >>   OneWireIOException: Device Not Present
    +	 *     >> my label
    +	 *     >> OneWireIOException: Device Not Present
     	 * 
    * - * @param lbl the message to print out above the array - * @param bytes the byte array to print out - * @param offset the offset to start printing from the array - * @param length the number of bytes to print from the array + * @param lbl the message to print out above the array + * @param t */ public static final void debug(String lbl, Throwable t) { if (DEBUG) { diff --git a/io.openems.edge.common/src/io/openems/edge/common/channel/Channel.java b/io.openems.edge.common/src/io/openems/edge/common/channel/Channel.java index 7a775ec3b04..9da11c47245 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/channel/Channel.java +++ b/io.openems.edge.common/src/io/openems/edge/common/channel/Channel.java @@ -21,15 +21,16 @@ *
      *
    • a Channel-ID which is unique among the OpenemsComponent. (see * {@link io.openems.edge.common.channel.ChannelId}) - *
    • a {@link Doc} as static meta information. (via {@link #channelDoc()}) + *
    • a {@link Doc} as static meta information. (via + * {@link Channel#channelDoc()}) *
    • a system-wide unique {@link ChannelAddress} built from Component-ID and - * Channel-ID. (via {@link #address()} + * Channel-ID. (via {@link Channel#address()} *
    • a {@link OpenemsType} which needs to map to the generic parameter - * <T>. (via {@link #getType()}) - *
    • an (active) {@link Value}. (via {@link #value()}) + * <T>. (via {@link Channel#getType()}) + *
    • an (active) {@link Value}. (via {@link Channel#value()}) *
    • callback methods to listen on value updates and changes. (see - * {@link #onChange(Consumer)}, {@link #onUpdate(Consumer)} and - * {@link #onSetNextValue(Consumer)}) + * {@link Channel#onChange(Consumer)}, {@link Channel#onUpdate(Consumer)} and + * {@link Channel#onSetNextValue(Consumer)}) *
    * *

    diff --git a/io.openems.edge.common/src/io/openems/edge/common/channel/ChannelId.java b/io.openems.edge.common/src/io/openems/edge/common/channel/ChannelId.java index 129ab8efab6..d7a707bead4 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/channel/ChannelId.java +++ b/io.openems.edge.common/src/io/openems/edge/common/channel/ChannelId.java @@ -38,12 +38,31 @@ public static String channelIdUpperToCamel(String name) { } } + /** + * Converts a Channel-ID in UPPER_CAMEL format to the UPPER_UNDERSCORE format. + * + *

    + * Examples: converts "ActivePower" to "ACTIVE_POWER". + * + * @param name Channel-ID in UPPER_CAMEL format. + * @return the a Channel-ID in UPPER_UNDERSCORE format + */ + public static String channelIdCamelToUpper(String name) { + if (name.startsWith("_")) { + // special handling for reserved Channel-IDs starting with "_". + return "_" + CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, name.substring(1)); + } else { + return CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, name); + } + } + /** * Lists all Channel-IDs of the given Channel-ID Enum in a form that is suitable * for a InfluxDB-Query in a Grafana Dashboard. * *

    - * To create a query, call this function like `ChannelId.printChannelIdsForInfluxQuery(FeneconMiniEss.ServiceInfoChannelId.values());` + * To create a query, call this function like + * `ChannelId.printChannelIdsForInfluxQuery(FeneconMiniEss.ServiceInfoChannelId.values());` * * @param channelIds the {@link ChannelId}s, e.g. from ChannelId.values(). */ 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 afe266e37d4..2c2136d6660 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 @@ -16,13 +16,13 @@ * Possible meta information include: *

      *
    • access-mode (read-only/read-write/write-only) flag - * {@link #accessMode(AccessMode)}. Defaults to Read-Only. - *
    • expected OpenemsType via {@link #getType()} - *
    • descriptive text via {@link #getText()} - *
    • is debug mode activated via {@link #isDebug()} - *
    • callback on initialization of a Channel via {@link #getOnInitCallback()} + * {@link Doc#accessMode(AccessMode)}. Defaults to Read-Only. + *
    • expected OpenemsType via {@link Doc#getType()} + *
    • descriptive text via {@link Doc#getText()} + *
    • is debug mode activated via {@link Doc#isDebug()} + *
    • callback on initialization of a Channel via + * {@link Doc#getOnInitCallback()} *
    - * */ public interface Doc { diff --git a/io.openems.edge.common/src/io/openems/edge/common/channel/EnumDoc.java b/io.openems.edge.common/src/io/openems/edge/common/channel/EnumDoc.java index b67f1a7c591..0527ca8c07b 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/channel/EnumDoc.java +++ b/io.openems.edge.common/src/io/openems/edge/common/channel/EnumDoc.java @@ -85,7 +85,7 @@ public OptionsEnum getUndefinedOption() { * * @param name the name of the option. Comparison is case insensitive * @return the {@link OptionsEnum} - * @throws Throws OpenemsNamedException if there is no option with that name + * @throws OpenemsNamedException if there is no option with that name */ public OptionsEnum getOptionFromString(String name) throws OpenemsNamedException { for (OptionsEnum e : this.options) { @@ -101,7 +101,7 @@ public OptionsEnum getOptionFromString(String name) throws OpenemsNamedException * * @param name the name of the option. Comparison is case insensitive * @return the integer value of the {@link OptionsEnum} - * @throws Throws OpenemsNamedException if there is no option with that name + * @throws OpenemsNamedException if there is no option with that name */ public int getOptionValueFromString(String name) throws OpenemsNamedException { return this.getOptionFromString(name).getValue(); 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 1a331dbded5..a171a31fb1a 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 @@ -141,10 +141,9 @@ public AbstractDoc onInit(Consumer> callback) { } /** - * Gets the callbacks for initialization of the actual Channel + * Gets the callbacks for initialization of the actual Channel. * - * @param callback the method to call on initialization - * @return myself + * @return a list of callbacks */ protected List>> getOnInitCallbacks() { return onInitCallback; diff --git a/io.openems.edge.common/src/io/openems/edge/common/component/ComponentManager.java b/io.openems.edge.common/src/io/openems/edge/common/component/ComponentManager.java index ac4797f7175..08dc3de5cff 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/component/ComponentManager.java +++ b/io.openems.edge.common/src/io/openems/edge/common/component/ComponentManager.java @@ -173,6 +173,15 @@ public default void _setDefaultConfigurationFailed(boolean value) { */ public List getEnabledComponents(); + /** + * Gets all enabled OpenEMS-Components of the given Type. + * + * @param the given Type, subclass of {@link OpenemsComponent} + * @param clazz the given Type, subclass of {@link OpenemsComponent} + * @return a List of OpenEMS-Components + */ + public List getEnabledComponentsOfType(Class clazz); + /** * Gets all OpenEMS-Components. * diff --git a/io.openems.edge.common/src/io/openems/edge/common/component/OpenemsComponent.java b/io.openems.edge.common/src/io/openems/edge/common/component/OpenemsComponent.java index e8f03e152a7..c431421dfa1 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/component/OpenemsComponent.java +++ b/io.openems.edge.common/src/io/openems/edge/common/component/OpenemsComponent.java @@ -388,7 +388,11 @@ public static void updateConfigurationProperty(ConfigurationAdmin cm, String pid */ public static void logDebug(OpenemsComponent component, Logger log, String message) { // TODO use log.debug(String, Object...) to improve speed - log.debug("[" + component.id() + "] " + message); + if (component != null) { + log.debug("[" + component.id() + "] " + message); + } else { + log.debug(message); + } } /** @@ -399,7 +403,11 @@ public static void logDebug(OpenemsComponent component, Logger log, String messa * @param message the message */ public static void logInfo(OpenemsComponent component, Logger log, String message) { - log.info("[" + component.id() + "] " + message); + if (component != null) { + log.info("[" + component.id() + "] " + message); + } else { + log.info(message); + } } /** @@ -410,7 +418,11 @@ public static void logInfo(OpenemsComponent component, Logger log, String messag * @param message the message */ public static void logWarn(OpenemsComponent component, Logger log, String message) { - log.warn("[" + component.id() + "] " + message); + if (component != null) { + log.warn("[" + component.id() + "] " + message); + } else { + log.warn(message); + } } /** @@ -421,7 +433,11 @@ public static void logWarn(OpenemsComponent component, Logger log, String messag * @param message the message */ public static void logError(OpenemsComponent component, Logger log, String message) { - log.error("[" + component.id() + "] " + message); + if (component != null) { + log.error("[" + component.id() + "] " + message); + } else { + log.error(message); + } } } diff --git a/io.openems.edge.common/src/io/openems/edge/common/linecharacteristic/PolyLine.java b/io.openems.edge.common/src/io/openems/edge/common/linecharacteristic/PolyLine.java index b7d40562ea6..8f6d0f4054b 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/linecharacteristic/PolyLine.java +++ b/io.openems.edge.common/src/io/openems/edge/common/linecharacteristic/PolyLine.java @@ -8,6 +8,7 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.utils.JsonUtils; +import io.openems.edge.common.type.TypeUtils; /** * Defines a polyline built of multiple points defined by a JsonArray. @@ -17,7 +18,57 @@ */ public class PolyLine { - private final TreeMap points; + private final TreeMap points; + + public static class Builder { + private final TreeMap points = new TreeMap<>(); + + private Builder() { + } + + public Builder addPoint(double x, Double y) { + this.points.put(x, y); + return this; + } + + public Builder addPoint(double x, double y) { + this.points.put(x, y); + return this; + } + + public PolyLine build() { + return new PolyLine(this.points); + } + } + + /** + * Create a PolyLine builder. + * + * @return a {@link Builder} + */ + public static Builder create() { + return new Builder(); + } + + /** + * Create a PolyLine that returns null for every 'x'. + * + * @return a {@link PolyLine} + */ + public static PolyLine empty() { + return new PolyLine((Double) null); + } + + /** + * Creates a static PolyLine, i.e. the 'y' value is the same for each 'x'. + * + * @param y 'y' value + */ + public PolyLine(Double y) { + TreeMap points = new TreeMap<>(); + points.put(0D, y); + this.points = points; + } /** * Creates a PolyLine from two points. @@ -26,15 +77,23 @@ public class PolyLine { * @param y1 'y' value of point 1 * @param x2 'x' value of point 2 * @param y2 'y' value of point 2 - * @throws OpenemsNamedException on error */ - public PolyLine(Float x1, Float y1, Float x2, Float y2) throws OpenemsNamedException { - TreeMap points = new TreeMap<>(); + public PolyLine(double x1, Double y1, double x2, Double y2) { + TreeMap points = new TreeMap<>(); points.put(x1, y1); points.put(x2, y2); this.points = points; } + /** + * Creates a PolyLine from a map of points. + * + * @param points a map of points + */ + public PolyLine(TreeMap points) { + this.points = points; + } + /** * Creates a PolyLine from a JSON line configuration. * @@ -69,10 +128,10 @@ public PolyLine(String x, String y, String lineConfig) throws OpenemsNamedExcept * @throws OpenemsNamedException on error */ public PolyLine(String x, String y, JsonArray lineConfig) throws OpenemsNamedException { - TreeMap points = new TreeMap<>(); + TreeMap points = new TreeMap<>(); for (JsonElement element : lineConfig) { - Float xValue = JsonUtils.getAsFloat(element, x); - Float yValue = JsonUtils.getAsFloat(element, y); + Double xValue = JsonUtils.getAsDouble(element, x); + Double yValue = JsonUtils.getAsDouble(element, y); points.put(xValue, yValue); } this.points = points; @@ -81,13 +140,16 @@ public PolyLine(String x, String y, JsonArray lineConfig) throws OpenemsNamedExc /** * Gets the Y-value for the given X. * - * @param x the 'x' value - * @return the 'y' value - * @throws OpenemsNamedException on error + * @param x the 'x' value, possibly null + * @return the 'y' value, possibly null */ - public Float getValue(float x) throws OpenemsNamedException { - Entry floorEntry = this.points.floorEntry(x); - Entry ceilingEntry = this.points.ceilingEntry(x); + public Double getValue(Double x) { + if (x == null) { + return null; + } + + Entry floorEntry = this.points.floorEntry(x); + Entry ceilingEntry = this.points.ceilingEntry(x); if (floorEntry == null && ceilingEntry == null) { return null; @@ -102,9 +164,39 @@ public Float getValue(float x) throws OpenemsNamedException { return floorEntry.getValue(); } else { - Float m = (ceilingEntry.getValue() - floorEntry.getValue()) / (ceilingEntry.getKey() - floorEntry.getKey()); - Float t = floorEntry.getValue() - m * floorEntry.getKey(); + Double m = (ceilingEntry.getValue() - floorEntry.getValue()) + / (ceilingEntry.getKey() - floorEntry.getKey()); + Double t = floorEntry.getValue() - m * floorEntry.getKey(); return m * x + t; } } + + /** + * Gets the Y-value for the given X. Convenience method that internally converts + * the Float to a Double. + * + * @param x the 'x' value, possibly null + * @return the 'y' value, possibly null + */ + public Double getValue(Float x) { + return this.getValue(TypeUtils.toDouble(x)); + } + + /** + * Gets the Y-value for the given X. Convenience method that internally converts + * the Integer to a Double. + * + * @param x the 'x' value, possibly null + * @return the 'y' value, possibly null + */ + public Double getValue(Integer x) { + return this.getValue(TypeUtils.toDouble(x)); + } + + public static void printAsCsv(PolyLine polyLine) { + System.out.println("x;y"); + for (Entry point : polyLine.points.entrySet()) { + System.out.println(point.getKey() + ";" + point.getValue()); + } + } } diff --git a/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractContext.java b/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractContext.java index 558d7a37b81..33554a9c8b4 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractContext.java +++ b/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractContext.java @@ -8,6 +8,13 @@ public class AbstractContext { private final PARENT parent; + /** + * Constructs an {@link AbstractContext} without useful logging. + */ + public AbstractContext() { + this(null); + } + /** * Constructs an {@link AbstractContext}. * @@ -24,7 +31,7 @@ public AbstractContext(PARENT parent) { * @return the parent */ public PARENT getParent() { - return parent; + return this.parent; } /** diff --git a/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractStateMachine.java b/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractStateMachine.java index 3a3938b3835..87a89a8f854 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractStateMachine.java +++ b/io.openems.edge.common/src/io/openems/edge/common/statemachine/AbstractStateMachine.java @@ -110,7 +110,7 @@ public void run(CONTEXT context) throws OpenemsNamedException { // Call StateMachine events on transition if (lastState != this.state) { - this.log.info("Changing StateMachine from [" + lastState + "] to [" + this.state + "]"); + context.logInfo(this.log, "Changing StateMachine from [" + lastState + "] to [" + this.state + "]"); // On-Exit of the last State try { diff --git a/io.openems.edge.common/src/io/openems/edge/common/statemachine/StateHandler.java b/io.openems.edge.common/src/io/openems/edge/common/statemachine/StateHandler.java index f0626af7545..f3ece76d7db 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/statemachine/StateHandler.java +++ b/io.openems.edge.common/src/io/openems/edge/common/statemachine/StateHandler.java @@ -13,7 +13,7 @@ public abstract class StateHandler, CONTEXT> { /** * Runs the main logic of StateMachine State and returns the next State. * - * @param context the {@link Context}. + * @param context the {@link CONTEXT}. * @return the next State */ protected abstract STATE runAndGetNextState(CONTEXT context) throws OpenemsNamedException; diff --git a/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java b/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java index 1f7e1f6b246..4b4eef0faab 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java +++ b/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java @@ -5,6 +5,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; +import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalUnit; import java.util.ArrayList; import java.util.Arrays; @@ -25,6 +26,7 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.function.ThrowingRunnable; import io.openems.common.types.ChannelAddress; import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.Channel; @@ -32,6 +34,8 @@ import io.openems.edge.common.channel.EnumDoc; import io.openems.edge.common.channel.WriteChannel; import io.openems.edge.common.channel.value.Value; +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; @@ -53,17 +57,27 @@ public ChannelValue(ChannelAddress address, Object value) { this.value = value; } + /** + * Gets the {@link ChannelAddress}. + * + * @return the {@link ChannelAddress} + */ public ChannelAddress getAddress() { - return address; + return this.address; } + /** + * Gets the value {@link Object}. + * + * @return the {@link Object} + */ public Object getValue() { - return value; + return this.value; } @Override public String toString() { - return address.toString() + ":" + value; + return this.address.toString() + ":" + this.value; } } @@ -95,6 +109,14 @@ public static class TestCase { private final String description; private final List inputs = new ArrayList<>(); private final List outputs = new ArrayList<>(); + private final List> onBeforeProcessImageCallbacks = new ArrayList<>(); + private final List> onAfterProcessImageCallbacks = new ArrayList<>(); + private final List> onBeforeControllersCallbacks = new ArrayList<>(); + private final List> onExecuteControllersCallbacks = new ArrayList<>(); + private final List> onAfterControllersCallbacks = new ArrayList<>(); + private final List> onBeforeWriteCallbacks = new ArrayList<>(); + private final List> onExecuteWriteCallbacks = new ArrayList<>(); + private final List> onAfterWriteCallbacks = new ArrayList<>(); private TimeLeap timeleap = null; @@ -111,21 +133,143 @@ public TestCase(String description) { this.description = "#" + (++instanceCounter) + (description.isEmpty() ? "" : ": " + description); } + /** + * Adds an input value for a Channel. + * + * @param address the {@link ChannelAddress} + * @param value the value {@link Object} + * @return myself + */ public TestCase input(ChannelAddress address, Object value) { this.inputs.add(new ChannelValue(address, value)); return this; } + /** + * Adds an expected output value for a Channel. + * + * @param address the {@link ChannelAddress} + * @param value the value {@link Object} + * @return myself + */ public TestCase output(ChannelAddress address, Object value) { this.outputs.add(new ChannelValue(address, value)); return this; } + /** + * Adds a simulated timeleap, i.e. simulates that a given amount of time passed. + * + * @param clock the active {@link TimeLeapClock}, i.e. provided to the + * system-under-test by a {@link ClockProvider} like + * {@link ComponentManager}. + * @param amountToAdd the amount that should be simulated + * @param unit the {@link TemporalUnit} of the amount, e.g. using the + * {@link ChronoUnit} enum + * @return myself + */ public TestCase timeleap(TimeLeapClock clock, long amountToAdd, TemporalUnit unit) { this.timeleap = new TimeLeap(clock, amountToAdd, unit); return this; } + /** + * Adds a Callback that is called on + * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_PROCESS_IMAGE} event. + * + * @param callback the callback + * @return myself + */ + public TestCase onBeforeProcessImage(ThrowingRunnable callback) { + this.onBeforeProcessImageCallbacks.add(callback); + return this; + } + + /** + * Adds a Callback that is called on + * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_PROCESS_IMAGE} event. + * + * @param callback the callback + * @return myself + */ + public TestCase onAfterProcessImage(ThrowingRunnable callback) { + this.onAfterProcessImageCallbacks.add(callback); + return this; + } + + /** + * Adds a Callback that is called on + * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_CONTROLLERS} event. + * + * @param callback the callback + * @return myself + */ + public TestCase onBeforeControllersCallbacks(ThrowingRunnable callback) { + this.onBeforeControllersCallbacks.add(callback); + return this; + } + + /** + * Adds a Callback that is called after + * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_CONTROLLERS} and before + * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_CONTROLLERS}. events. + * + * @param callback the callback + * @return myself + */ + public TestCase onExecuteControllersCallbacks(ThrowingRunnable callback) { + this.onExecuteControllersCallbacks.add(callback); + return this; + } + + /** + * Adds a Callback that is called on + * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_CONTROLLERS} event. + * + * @param callback the callback + * @return myself + */ + public TestCase onAfterControllersCallbacks(ThrowingRunnable callback) { + this.onAfterControllersCallbacks.add(callback); + return this; + } + + /** + * Adds a Callback that is called on + * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_WRITE} event. + * + * @param callback the callback + * @return myself + */ + public TestCase onBeforeWriteCallbacks(ThrowingRunnable callback) { + this.onBeforeWriteCallbacks.add(callback); + return this; + } + + /** + * Adds a Callback that is called on + * {@link EdgeEventConstants#TOPIC_CYCLE_EXECUTE_WRITE} event. + * + * @param callback the callback + * @return myself + */ + public TestCase onExecuteWriteCallbacks(ThrowingRunnable callback) { + this.onExecuteWriteCallbacks.add(callback); + return this; + } + + /** + * Adds a Callback that is called on + * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_WRITE} event. + * + * @param callback the callback + * @return myself + */ + public TestCase onAfterWriteCallbacks(ThrowingRunnable callback) { + this.onAfterWriteCallbacks.add(callback); + return this; + } + /** * Applies the time leap to the clock. */ @@ -164,7 +308,6 @@ protected void applyInputs(Map components) * Validates the output values. * * @param components Referenced components - * @param index * @throws Exception on validation failure */ protected void validateOutputs(Map components) throws Exception { @@ -301,13 +444,35 @@ public SELF addReference(String memberName, Object object) throws Exception { return this.self(); } + private boolean addReference(Class clazz, String memberName, Object object) + throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + try { + Field field = clazz.getDeclaredField(memberName); + field.setAccessible(true); + field.set(this.sut, object); + return true; + } catch (NoSuchFieldException e) { + // Ignore. Try method. + if (this.invokeSingleArgMethod(clazz, memberName, object)) { + return true; + } + } + // If we are here, no matching field or method was found. Search in parent + // classes. + Class parent = clazz.getSuperclass(); + if (parent == null) { + return false; // reached 'java.lang.Object' + } + return this.addReference(parent, memberName, object); + } + /** * Adds an available {@link OpenemsComponent}. * *

    * If the provided Component is a {@link DummyComponentManager}. * - * @param component + * @param component the {@link OpenemsComponent}s * @return itself, to use as a builder */ public SELF addComponent(OpenemsComponent component) { @@ -352,7 +517,7 @@ public SELF activate(AbstractComponentConfig config) throws Exception { } // Now SUT can be added to the list, as it does have an ID now - this.addComponent(sut); + this.addComponent(this.sut); return this.self(); } @@ -411,28 +576,6 @@ private void callDeactivate() throws IllegalAccessException, IllegalArgumentExce method.invoke(this.sut); } - private boolean addReference(Class clazz, String memberName, Object object) - throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { - try { - Field field = clazz.getDeclaredField(memberName); - field.setAccessible(true); - field.set(this.sut, object); - return true; - } catch (NoSuchFieldException e) { - // Ignore. Try method. - if (this.invokeSingleArgMethod(clazz, memberName, object)) { - return true; - } - } - // If we are here, no matching field or method was found. Search in parent - // classes. - Class parent = clazz.getSuperclass(); - if (parent == null) { - return false; // reached 'java.lang.Object' - } - return addReference(parent, memberName, object); - } - private boolean invokeSingleArgMethod(Class clazz, String methodName, Object arg) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { Method[] methods = clazz.getDeclaredMethods(); @@ -468,28 +611,42 @@ private boolean invokeSingleArgMethod(Class clazz, String methodName, Object public SELF next(TestCase testCase) throws Exception { testCase.applyTimeLeap(); this.onBeforeProcessImage(); + executeCallbacks(testCase.onBeforeProcessImageCallbacks); this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE); for (Channel channel : this.getSut().channels()) { channel.nextProcessImage(); } testCase.applyInputs(this.components); this.onAfterProcessImage(); + executeCallbacks(testCase.onAfterProcessImageCallbacks); this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE); this.onBeforeControllers(); + executeCallbacks(testCase.onBeforeControllersCallbacks); this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_BEFORE_CONTROLLERS); this.onExecuteControllers(); + executeCallbacks(testCase.onExecuteControllersCallbacks); this.onAfterControllers(); + executeCallbacks(testCase.onAfterControllersCallbacks); this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_AFTER_CONTROLLERS); this.onBeforeWrite(); + executeCallbacks(testCase.onBeforeWriteCallbacks); this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_BEFORE_WRITE); this.onExecuteWrite(); + executeCallbacks(testCase.onExecuteWriteCallbacks); this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE); this.onAfterWrite(); + executeCallbacks(testCase.onAfterWriteCallbacks); this.handleEvent(EdgeEventConstants.TOPIC_CYCLE_AFTER_WRITE); testCase.validateOutputs(this.components); return this.self(); } + private static void executeCallbacks(List> callbacks) throws Exception { + for (ThrowingRunnable callback : callbacks) { + callback.run(); + } + } + /** * If the 'system-under-test' is a {@link EventHandler} call the * {@link EventHandler#handleEvent(Event)} method. @@ -507,7 +664,7 @@ protected void handleEvent(String topic) throws Exception { /** * This method is executed before the - * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_PROCESS_IMAGE event. + * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_PROCESS_IMAGE} event. * * @throws OpenemsNamedException on error */ @@ -516,7 +673,7 @@ protected void onBeforeProcessImage() throws OpenemsNamedException { /** * This method is executed before the - * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_PROCESS_IMAGE event. + * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_PROCESS_IMAGE} event. * * @throws OpenemsNamedException on error */ @@ -525,7 +682,7 @@ protected void onAfterProcessImage() throws OpenemsNamedException { /** * This method is executed before the - * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_CONTROLLERS event. + * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_CONTROLLERS} event. * * @throws OpenemsNamedException on error */ @@ -533,8 +690,9 @@ protected void onBeforeControllers() throws OpenemsNamedException { } /** - * This method is executed after TOPIC_CYCLE_BEFORE_CONTROLLERS and before - * TOPIC_CYCLE_AFTER_CONTROLLERS. + * This method is executed after + * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_CONTROLLERS} and before + * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_CONTROLLERS}. * * @throws OpenemsNamedException on error */ @@ -543,7 +701,7 @@ protected void onExecuteControllers() throws OpenemsNamedException { /** * This method is executed before the - * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_CONTROLLERS event. + * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_CONTROLLERS} event. * * @throws OpenemsNamedException on error */ @@ -552,7 +710,7 @@ protected void onAfterControllers() throws OpenemsNamedException { /** * This method is executed before the - * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_WRITE event. + * {@link EdgeEventConstants#TOPIC_CYCLE_BEFORE_WRITE} event. * * @throws OpenemsNamedException on error */ @@ -561,7 +719,7 @@ protected void onBeforeWrite() throws OpenemsNamedException { /** * This method is executed before the - * {@link EdgeEventConstants#TOPIC_CYCLE_EXECUTE_WRITE event. + * {@link EdgeEventConstants#TOPIC_CYCLE_EXECUTE_WRITE} event. * * @throws OpenemsNamedException on error */ @@ -570,11 +728,11 @@ protected void onExecuteWrite() throws OpenemsNamedException { /** * This method is executed before - * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_WRITE. + * {@link EdgeEventConstants#TOPIC_CYCLE_AFTER_WRITE}. * * @throws OpenemsNamedException on error */ - protected void onAfterWrite() { + protected void onAfterWrite() throws OpenemsNamedException { } diff --git a/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentManager.java b/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentManager.java index d43a68ead40..026a720dfba 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentManager.java +++ b/io.openems.edge.common/src/io/openems/edge/common/test/DummyComponentManager.java @@ -45,6 +45,18 @@ public List getAllComponents() { return Collections.unmodifiableList(this.components); } + @Override + @SuppressWarnings("unchecked") + public List getEnabledComponentsOfType(Class clazz) { + List result = new ArrayList<>(); + for (OpenemsComponent component : this.components) { + if (component.getClass().isInstance(clazz)) { + result.add((T) component); + } + } + return result; + } + /** * Specific for this Dummy implementation. * diff --git a/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java b/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java index 52b2fce7697..5f725ee0d8b 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java +++ b/io.openems.edge.common/src/io/openems/edge/common/type/TypeUtils.java @@ -7,7 +7,6 @@ import com.google.gson.JsonNull; import com.google.gson.JsonPrimitive; -import io.openems.common.exceptions.OpenemsException; import io.openems.common.types.OpenemsType; import io.openems.common.types.OptionsEnum; import io.openems.edge.common.channel.value.Value; @@ -376,9 +375,9 @@ public static Long sum(Long... values) { * Safely subtract Integers. * *

      - *
    • if minuend is null -> result is null - *
    • if subtrahend is null -> result is minuend - *
    • if both are null -> result is null + *
    • if minuend is null -> result is null + *
    • if subtrahend is null -> result is minuend + *
    • if both are null -> result is null *
    * * @param minuend the minuend of the subtraction @@ -399,9 +398,9 @@ public static Integer subtract(Integer minuend, Integer subtrahend) { * Safely subtract Longs. * *
      - *
    • if minuend is null -> result is null - *
    • if subtrahend is null -> result is minuend - *
    • if both are null -> result is null + *
    • if minuend is null -> result is null + *
    • if subtrahend is null -> result is minuend + *
    • if both are null -> result is null *
    * * @param minuend the minuend of the subtraction @@ -436,15 +435,33 @@ public static Integer multiply(Integer... factors) { return result; } + /** + * Safely multiply Doubles. + * + * @param factors the factors of the multiplication + * @return the result, possibly null if all factors are null + */ + public static Double multiply(Double... factors) { + Double result = null; + for (Double factor : factors) { + if (result == null) { + result = factor; + } else if (factor != null) { + result *= factor; + } + } + return result; + } + /** * Safely divides Integers. * *
      - *
    • if dividend is null -> result is null + *
    • if dividend is null -> result is null *
    * - * @param minuend the dividend of the division - * @param subtrahend the divisor of the division + * @param dividend the dividend of the division + * @param divisor the divisor of the division * @return the result, possibly null */ public static Integer divide(Integer dividend, int divisor) { @@ -458,11 +475,11 @@ public static Integer divide(Integer dividend, int divisor) { * Safely divides Longs. * *
      - *
    • if dividend is null -> result is null + *
    • if dividend is null -> result is null *
    * - * @param minuend the dividend of the division - * @param subtrahend the divisor of the division + * @param dividend the dividend of the division + * @param divisor the divisor of the division * @return the result, possibly null */ public static Long divide(Long dividend, long divisor) { @@ -491,6 +508,25 @@ public static Integer max(Integer... values) { return result; } + /** + * Safely finds the min value of all values. + * + * @return the min value; or null if all values are null + */ + public static Double min(Double... values) { + Double result = null; + for (Double value : values) { + if (value != null) { + if (result == null) { + result = value; + } else { + result = Math.min(result, value); + } + } + } + return result; + } + /** * Safely finds the average value of all values. * @@ -511,6 +547,28 @@ public static Float average(Integer... values) { return sum / count; } + /** + * Safely finds the average value of all values. + * + * @return the average value; or Double.NaN if all values are invalid. + */ + public static double average(double... values) { + int count = 0; + double sum = 0.; + for (double value : values) { + if (Double.isNaN(value)) { + continue; + } else { + count++; + sum += value; + } + } + if (count == 0) { + return Double.NaN; + } + return sum / count; + } + /** * Safely finds the average value of all values and rounds the result to an * Integer using {@link Math#round(float)}. @@ -527,15 +585,45 @@ public static Integer averageRounded(Integer... values) { } /** - * Throws an descriptive exception if the object is null. + * Throws a descriptive exception if any of the objects is null. * * @param description text that is added to the exception - * @param object the object - * @throws OpenemsException if object is null + * @param objects the objects + * @throws IllegalArgumentException if any object is null */ - public static void assertNull(String description, Object object) throws OpenemsException { - if (object == null) { - throw new OpenemsException(description + " value is null!"); + public static void assertNull(String description, Object... objects) throws IllegalArgumentException { + for (Object object : objects) { + if (object == null) { + throw new IllegalArgumentException(description + ": value is null!"); + } + } + } + + /** + * Safely convert from {@link Integer} to {@link Double} + * + * @param value the Integer value, possibly null + * @return the Double value, possibly null + */ + public static Double toDouble(Integer value) { + if (value == null) { + return (Double) null; + } else { + return Double.valueOf(value); + } + } + + /** + * Safely convert from {@link Float} to {@link Double} + * + * @param value the Float value, possibly null + * @return the Double value, possibly null + */ + public static Double toDouble(Float value) { + if (value == null) { + return (Double) null; + } else { + return Double.valueOf(value); } } diff --git a/io.openems.edge.common/test/io/openems/edge/common/linecharacteristic/PolyLineTest.java b/io.openems.edge.common/test/io/openems/edge/common/linecharacteristic/PolyLineTest.java index 188806d300e..e805742f4c8 100644 --- a/io.openems.edge.common/test/io/openems/edge/common/linecharacteristic/PolyLineTest.java +++ b/io.openems.edge.common/test/io/openems/edge/common/linecharacteristic/PolyLineTest.java @@ -35,19 +35,19 @@ public void test() throws OpenemsNamedException { PolyLine polyline = new PolyLine("xCoord", "yCoord", lineConfig); // exactly first - assertEquals(60f, polyline.getValue(0.9f), 0.00001); + assertEquals(60f, polyline.getValue(0.9), 0.00001); // exactly last - assertEquals(-60f, polyline.getValue(1.1f), 0.00001); + assertEquals(-60f, polyline.getValue(1.1), 0.00001); // beyond last - assertEquals(-60f, polyline.getValue(1.2f), 0.00001); + assertEquals(-60f, polyline.getValue(1.2), 0.00001); // before first - assertEquals(60f, polyline.getValue(0.7f), 0.00001); + assertEquals(60f, polyline.getValue(0.7), 0.00001); // between first two - assertEquals(30f, polyline.getValue(0.915f), 0.001); + assertEquals(30f, polyline.getValue(0.915), 0.001); } @Test @@ -57,7 +57,7 @@ public void testEmpty() throws OpenemsNamedException { PolyLine polyline = new PolyLine("xCoord", "yCoord", lineConfig); // exactly first - assertEquals(null, polyline.getValue(0.9f)); + assertEquals(null, polyline.getValue(0.9)); } } diff --git a/io.openems.edge.common/test/io/openems/edge/common/type/TypeUtilsTest.java b/io.openems.edge.common/test/io/openems/edge/common/type/TypeUtilsTest.java index c424e333a0e..e7b35ac3957 100644 --- a/io.openems.edge.common/test/io/openems/edge/common/type/TypeUtilsTest.java +++ b/io.openems.edge.common/test/io/openems/edge/common/type/TypeUtilsTest.java @@ -8,17 +8,14 @@ public class TypeUtilsTest { @Test public void testAverage() { - // no values - assertEquals(null, TypeUtils.average()); - // null values assertEquals(null, TypeUtils.average(null, null, null)); // int value - assertEquals(2, Math.round(TypeUtils.average(1, 2, 3))); + assertEquals(Integer.valueOf(2), TypeUtils.averageRounded(1, 2, 3)); // float values - assertEquals(2.5f, TypeUtils.average(2, 3), 0.001); + assertEquals(2.5f, TypeUtils.average(2F, 3F), 0.001); // mixed values assertEquals(2.5f, TypeUtils.average(2, null, 3), 0.001); diff --git a/io.openems.edge.controller.api.modbus/readme.adoc b/io.openems.edge.controller.api.modbus/readme.adoc index 95734ed7e0d..89d965f1dd4 100644 --- a/io.openems.edge.controller.api.modbus/readme.adoc +++ b/io.openems.edge.controller.api.modbus/readme.adoc @@ -1,5 +1,29 @@ = Api Modbus -Provides a Modbus-Slave implementation for OpenEMS Edge. It provides access to Channels from an external device via Modbus/TCP. +The OpenEMS Edge Modbus-Slave-API is provided by the "Modbus-API Controller". +As the Modbus protocol is widely used in industrial monitoring and automation, this allows for easy access to OpenEMS channels from external systems. + +The configuration of the Modbus-API controller defines which OpenEMS Components should be exported and made available via the API. +It then generates a dynamic Modbus protocol that is structured in address blocks that map to OpenEMS Components and Modbus register addresses that map to OpenEMS channels. + +The modbus table is designed in a way that allows dynamic parsing of all available registers. + +The following example describes a Controller that is configured to export the Sum-Component (`_sum`). By reading the headers of the individual blocks, the entire Modbus protocol can be parsed dynamically: + +. Register `0` identifies the system as an OpenEMS by the hash value `0x17ed6201`. + +. Register `1` shows the length of the first block. Adding the length (`199`) the current address (`1`) gives the starting address of the next block (`200`). + +. Register `200` gives a string with fixed length of 16 characters with the Component-ID. + +. Register `216` shows the length of the complete block. Adding the length (`300`) to the starting address of the block (`200`) gives the starting address of the next block (`500`) and so forth. + +. Register `220` identifies the first sub-block as Nature `OpenemsComponent`. The length of the sub-block follows in Register `221` and gives the starting address of the next sub-block (`300`) and so on. + +Instead of parsing the Modbus protocol, it is also possible to download the EMS specific Excel file via OpenEMS UI "System Profile" menu. An example export is available in the 'doc' folder of this bundle. + +To communicate with specific channels, it is then sufficient to read or write to the matching registers, e.g. +- Read register `302 _sum/EssSoc` to get the total average state of charge of the ESS. +- Write to register `806 ess0/SetActivePowerEquals` to trigger charging or discharging of the ESS with ID `ess0`. https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.controller.api.modbus[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/ActivePowerVoltageCharacteristicImpl.java b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/ActivePowerVoltageCharacteristicImpl.java index ac4a9b0fdff..3f41c2351ea 100644 --- a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/ActivePowerVoltageCharacteristicImpl.java +++ b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/src/io/openems/edge/controller/ess/activepowervoltagecharacteristic/ActivePowerVoltageCharacteristicImpl.java @@ -125,7 +125,7 @@ public void run() throws OpenemsNamedException { if (this.pByUCharacteristics == null) { power = null; } else { - Float p = this.pByUCharacteristics.getValue(voltageRatio); + Double p = this.pByUCharacteristics.getValue(voltageRatio); if (p == null) { power = null; } else { diff --git a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/CharacteristicImplTest.java b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/CharacteristicImplTest.java index 8506905317a..ca31132129f 100644 --- a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/CharacteristicImplTest.java +++ b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/CharacteristicImplTest.java @@ -67,11 +67,11 @@ public void test() throws Exception { ).build()) // .next(new TestCase("First Input") // .input(METER_VOLTAGE, 250_000) // [mV] - .output(ESS_ACTIVE_POWER, -2750)) // + .output(ESS_ACTIVE_POWER, -2749)) // .next(new TestCase("Second Input, \"Power: -1500 \"") // .timeleap(clock, 5, ChronoUnit.SECONDS) // .input(METER_VOLTAGE, 248_000) // [mV] - .output(ESS_ACTIVE_POWER, -1500))// + .output(ESS_ACTIVE_POWER, -1499))// .next(new TestCase() // .input(METER_VOLTAGE, 240_200) // [mV] .output(ESS_ACTIVE_POWER, null)) // @@ -88,7 +88,7 @@ public void test() throws Exception { .next(new TestCase("Fourth Input, \"Power: 0 \"") // .timeleap(clock, 5, ChronoUnit.SECONDS) // .input(METER_VOLTAGE, 235_200) // [mV] - .output(ESS_ACTIVE_POWER, 1000)) // + .output(ESS_ACTIVE_POWER, 998)) // .next(new TestCase() // .timeleap(clock, 2, ChronoUnit.SECONDS) // .input(METER_VOLTAGE, 235_600) // [mV] diff --git a/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/AbstractPredictiveDelayCharge.java b/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/AbstractPredictiveDelayCharge.java index bcdfac2eb02..208e36abbb0 100644 --- a/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/AbstractPredictiveDelayCharge.java +++ b/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/AbstractPredictiveDelayCharge.java @@ -15,8 +15,8 @@ import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.controller.api.Controller; import io.openems.edge.ess.api.ManagedSymmetricEss; -import io.openems.edge.predictor.api.ConsumptionHourlyPredictor; -import io.openems.edge.predictor.api.ProductionHourlyPredictor; +import io.openems.edge.predictor.api.hourly.ConsumptionHourlyPredictor; +import io.openems.edge.predictor.api.hourly.ProductionHourlyPredictor; public abstract class AbstractPredictiveDelayCharge extends AbstractOpenemsComponent implements OpenemsComponent { diff --git a/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/ac/AcPredictiveDelayCharge.java b/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/ac/AcPredictiveDelayCharge.java index 59bf5e0dd2a..d6cf1a77402 100644 --- a/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/ac/AcPredictiveDelayCharge.java +++ b/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/ac/AcPredictiveDelayCharge.java @@ -17,8 +17,8 @@ import io.openems.edge.ess.power.api.Phase; import io.openems.edge.ess.power.api.Pwr; import io.openems.edge.ess.power.api.Relationship; -import io.openems.edge.predictor.api.ConsumptionHourlyPredictor; -import io.openems.edge.predictor.api.ProductionHourlyPredictor; +import io.openems.edge.predictor.api.hourly.ConsumptionHourlyPredictor; +import io.openems.edge.predictor.api.hourly.ProductionHourlyPredictor; @Designate(ocd = Config.class, factory = true) @Component(// diff --git a/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/dc/DcPredictiveDelayCharge.java b/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/dc/DcPredictiveDelayCharge.java index efb8397a330..f2fef0611b0 100644 --- a/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/dc/DcPredictiveDelayCharge.java +++ b/io.openems.edge.controller.ess.predictivedelaycharge/src/io/openems/edge/controller/ess/predictivedelaycharge/dc/DcPredictiveDelayCharge.java @@ -18,8 +18,8 @@ import io.openems.edge.ess.power.api.Phase; import io.openems.edge.ess.power.api.Pwr; import io.openems.edge.ess.power.api.Relationship; -import io.openems.edge.predictor.api.ConsumptionHourlyPredictor; -import io.openems.edge.predictor.api.ProductionHourlyPredictor; +import io.openems.edge.predictor.api.hourly.ConsumptionHourlyPredictor; +import io.openems.edge.predictor.api.hourly.ProductionHourlyPredictor; @Designate(ocd = Config.class, factory = true) @Component(// @@ -51,6 +51,7 @@ void activate(ComponentContext context, Config config) throws OpenemsNamedExcept this.config = config; } +// @Deactivate protected void deactivate() { super.deactivate(); diff --git a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ReactivePwrVoltChractersticImpl.java b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ReactivePwrVoltChractersticImpl.java index 058a2a37b30..d5ac5a4721c 100644 --- a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ReactivePwrVoltChractersticImpl.java +++ b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/src/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ReactivePwrVoltChractersticImpl.java @@ -125,7 +125,7 @@ public void run() throws OpenemsNamedException { if (this.qByUCharacteristics == null) { percent = null; } else { - Float p = this.qByUCharacteristics.getValue(voltageRatio); + Double p = this.qByUCharacteristics.getValue(voltageRatio); if (p == null) { percent = null; } else { diff --git a/io.openems.edge.core/bnd.bnd b/io.openems.edge.core/bnd.bnd index c4ff726c562..f492dc3d930 100644 --- a/io.openems.edge.core/bnd.bnd +++ b/io.openems.edge.core/bnd.bnd @@ -10,6 +10,7 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.controller.api,\ io.openems.edge.ess.api,\ io.openems.edge.meter.api,\ + io.openems.edge.predictor.api,\ io.openems.edge.scheduler.api,\ io.openems.edge.timedata.api,\ io.openems.wrapper.sdnotify 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 d01d7d43f17..b6d91486094 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 @@ -132,6 +132,18 @@ public List getEnabledComponents() { return Collections.unmodifiableList(this.enabledComponents); } + @Override + @SuppressWarnings("unchecked") + public List getEnabledComponentsOfType(Class clazz) { + List result = new ArrayList<>(); + for (OpenemsComponent component : this.enabledComponents) { + if (component.getClass().isInstance(clazz)) { + result.add((T) component); + } + } + return result; + } + @Override public List getAllComponents() { return Collections.unmodifiableList(this.allComponents); diff --git a/io.openems.edge.core/src/io/openems/edge/core/componentmanager/OutOfMemoryHeapDumpWorker.java b/io.openems.edge.core/src/io/openems/edge/core/componentmanager/OutOfMemoryHeapDumpWorker.java index e338e38dbf6..3e87bdd76c8 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/componentmanager/OutOfMemoryHeapDumpWorker.java +++ b/io.openems.edge.core/src/io/openems/edge/core/componentmanager/OutOfMemoryHeapDumpWorker.java @@ -12,7 +12,7 @@ * This Worker constantly checks for heap-dump files in /usr/lib/openems * directory. Those get created on OutOfMemory-Errors. All but the latest * heap-dump file are deleted and the - * {@link ComponentManagerImpl.ChannelId#WAS_OUT_OF_MEMORY} StateChannel is set. + * {@link ComponentManager.ChannelId#WAS_OUT_OF_MEMORY} StateChannel is set. */ public class OutOfMemoryHeapDumpWorker extends ComponentManagerWorker { diff --git a/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemDebianSystemd.java b/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemDebianSystemd.java index 5609d8e8cd4..52569c3fe40 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemDebianSystemd.java +++ b/io.openems.edge.core/src/io/openems/edge/core/host/OperatingSystemDebianSystemd.java @@ -169,7 +169,7 @@ public NetworkConfiguration getNetworkConfiguration() throws OpenemsNamedExcepti interfaces.put(name.get(), new NetworkInterface(name.get(), // dhcp.get(), linkLocalAddressing.get(), gateway.get(), dns.get(), addresses.get(), file)); - } catch (IOException e) { + } catch (IllegalArgumentException | IOException e) { throw new OpenemsException("Unable to read file [" + file + "]: " + e.getMessage()); } } diff --git a/io.openems.edge.core/src/io/openems/edge/core/predictormanager/Get24HoursPredictionRequest.java b/io.openems.edge.core/src/io/openems/edge/core/predictormanager/Get24HoursPredictionRequest.java new file mode 100644 index 00000000000..eb34b821b1e --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/predictormanager/Get24HoursPredictionRequest.java @@ -0,0 +1,70 @@ +package io.openems.edge.core.predictormanager; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.jsonrpc.base.JsonrpcRequest; +import io.openems.common.types.ChannelAddress; +import io.openems.common.utils.JsonUtils; + +/** + * Wraps a JSON-RPC Request to query a 24 Hours Prediction. + * + *
    + * {
    + *   "jsonrpc": "2.0",
    + *   "id": "UUID",
    + *   "method": "get24HoursPrediction",
    + *   "params": {
    + *   	"channels": string[]
    + *   }
    + * }
    + * 
    + */ +public class Get24HoursPredictionRequest extends JsonrpcRequest { + + public static final String METHOD = "get24HoursPrediction"; + + public static Get24HoursPredictionRequest from(JsonrpcRequest r) throws OpenemsNamedException { + JsonObject p = r.getParams(); + JsonArray cs = JsonUtils.getAsJsonArray(p, "channels"); + List channels = new ArrayList<>(); + for (JsonElement c : cs) { + channels.add(ChannelAddress.fromString(JsonUtils.getAsString(c))); + } + return new Get24HoursPredictionRequest(r.getId(), channels); + } + + private final List channels; + + public Get24HoursPredictionRequest(List channels) { + this(UUID.randomUUID(), channels); + } + + public Get24HoursPredictionRequest(UUID id, List channels) { + super(id, METHOD); + this.channels = channels; + } + + @Override + public JsonObject getParams() { + JsonArray channels = new JsonArray(); + for (ChannelAddress channel : this.channels) { + channels.add(channel.toString()); + } + return JsonUtils.buildJsonObject() // + .add("channels", channels) // + .build(); + } + + public List getChannels() { + return this.channels; + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/predictormanager/Get24HoursPredictionResponse.java b/io.openems.edge.core/src/io/openems/edge/core/predictormanager/Get24HoursPredictionResponse.java new file mode 100644 index 00000000000..cf13edaaf9a --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/predictormanager/Get24HoursPredictionResponse.java @@ -0,0 +1,57 @@ +package io.openems.edge.core.predictormanager; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; +import io.openems.common.types.ChannelAddress; +import io.openems.edge.predictor.api.oneday.Prediction24Hours; + +/** + * Wraps a JSON-RPC Response to "get24HoursPrediction" Request. + * + *

    + * + *

    + * {
    + *   "jsonrpc": "2.0",
    + *   "id": "UUID",
    + *   "result": {
    + *     "componentId/channelId": [
    + *         value1, value2,... // 96 values; one value per 15 minutes
    + *     ]
    + *   }
    + * }
    + * 
    + */ +public class Get24HoursPredictionResponse extends JsonrpcResponseSuccess { + + private final Map predictions; + + public Get24HoursPredictionResponse(UUID id, Map predictions) { + super(id); + this.predictions = predictions; + } + + @Override + public JsonObject getResult() { + JsonObject j = new JsonObject(); + for (Entry entry : this.predictions.entrySet()) { + JsonArray values = new JsonArray(); + for (Integer value : entry.getValue().getValues()) { + values.add(value); + } + j.add(entry.getKey().toString(), values); + } + return j; + } + + public Map getPredictions() { + return predictions; + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/predictormanager/PredictorManagerImpl.java b/io.openems.edge.core/src/io/openems/edge/core/predictormanager/PredictorManagerImpl.java new file mode 100644 index 00000000000..09ba90ca6a1 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/predictormanager/PredictorManagerImpl.java @@ -0,0 +1,233 @@ +package io.openems.edge.core.predictormanager; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +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 io.openems.common.OpenemsConstants; +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.jsonrpc.base.JsonrpcRequest; +import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; +import io.openems.common.session.User; +import io.openems.common.types.ChannelAddress; +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.jsonapi.JsonApi; +import io.openems.edge.common.sum.Sum; +import io.openems.edge.ess.dccharger.api.EssDcCharger; +import io.openems.edge.meter.api.SymmetricMeter; +import io.openems.edge.predictor.api.manager.PredictorManager; +import io.openems.edge.predictor.api.oneday.Prediction24Hours; +import io.openems.edge.predictor.api.oneday.Predictor24Hours; + +@Component(// + name = "Core.PredictorManager", // + immediate = true, // + property = { // + "id=" + OpenemsConstants.PREDICTOR_MANAGER_ID, // + "enabled=true" // + }) +public class PredictorManagerImpl extends AbstractOpenemsComponent + implements PredictorManager, OpenemsComponent, JsonApi { + + @Reference + protected ComponentManager componentManager; + + @Reference(policy = ReferencePolicy.DYNAMIC, // + policyOption = ReferencePolicyOption.GREEDY, // + cardinality = ReferenceCardinality.MULTIPLE, // + target = "(enabled=true)") + private volatile List predictors = new CopyOnWriteArrayList<>(); + + public PredictorManagerImpl() { + super(// + OpenemsComponent.ChannelId.values(), // + PredictorManager.ChannelId.values() // + ); + } + + @Activate + void activate(ComponentContext context) { + super.activate(context, OpenemsConstants.PREDICTOR_MANAGER_ID, "Core.PredictorManager", true); + } + + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + public CompletableFuture handleJsonrpcRequest(User user, JsonrpcRequest request) + throws OpenemsNamedException { + switch (request.getMethod()) { + case Get24HoursPredictionRequest.METHOD: + return this.handleGet24HoursPredictionRequest(user, Get24HoursPredictionRequest.from(request)); + } + return null; + } + + private CompletableFuture handleGet24HoursPredictionRequest(User user, + Get24HoursPredictionRequest request) throws OpenemsNamedException { + final Map predictions = new HashMap<>(); + for (ChannelAddress channel : request.getChannels()) { + predictions.put(channel, this.get24HoursPrediction(channel)); + } + return CompletableFuture.completedFuture(new Get24HoursPredictionResponse(request.getId(), predictions)); + } + + @Override + public Prediction24Hours get24HoursPrediction(ChannelAddress channelAddress) { + Predictor24Hours predictor = this.getPredictorBestMatch(channelAddress); + if (predictor == null) { + // No explicit predictor found + if (channelAddress.getComponentId().equals(OpenemsConstants.SUM_ID)) { + // This is a Sum-Channel. Try to get predictions for each source channel. + Sum.ChannelId channelId = Sum.ChannelId.valueOf( + io.openems.edge.common.channel.ChannelId.channelIdCamelToUpper(channelAddress.getChannelId())); + return this.getPredictionSum(channelId); + } else { + return Prediction24Hours.EMPTY; + } + } else { + return predictor.get24HoursPrediction(channelAddress); + } + } + + /** + * Gets the {@link Prediction24Hours} for a Sum-Channel. + * + * @param channelId the {@link Sum.ChannelId} + * @return the {@link Prediction24Hours} + */ + private Prediction24Hours getPredictionSum(Sum.ChannelId channelId) { + switch (channelId) { + case CONSUMPTION_ACTIVE_ENERGY: + case CONSUMPTION_ACTIVE_POWER_L1: + case CONSUMPTION_ACTIVE_POWER_L2: + case CONSUMPTION_ACTIVE_POWER_L3: + case CONSUMPTION_MAX_ACTIVE_POWER: + case ESS_ACTIVE_CHARGE_ENERGY: + case ESS_ACTIVE_DISCHARGE_ENERGY: + case ESS_ACTIVE_POWER: + case ESS_ACTIVE_POWER_L1: + case ESS_ACTIVE_POWER_L2: + case ESS_ACTIVE_POWER_L3: + case ESS_CAPACITY: + case ESS_DC_CHARGE_ENERGY: + case ESS_DC_DISCHARGE_ENERGY: + case ESS_DISCHARGE_POWER: + case ESS_MAX_APPARENT_POWER: + case ESS_REACTIVE_POWER: + case ESS_SOC: + case GRID_ACTIVE_POWER: + case GRID_ACTIVE_POWER_L1: + case GRID_ACTIVE_POWER_L2: + case GRID_ACTIVE_POWER_L3: + case GRID_BUY_ACTIVE_ENERGY: + case GRID_MAX_ACTIVE_POWER: + case GRID_MIN_ACTIVE_POWER: + case GRID_MODE: + case GRID_SELL_ACTIVE_ENERGY: + case PRODUCTION_ACTIVE_ENERGY: + case PRODUCTION_AC_ACTIVE_ENERGY: + case PRODUCTION_AC_ACTIVE_POWER_L1: + case PRODUCTION_AC_ACTIVE_POWER_L2: + case PRODUCTION_AC_ACTIVE_POWER_L3: + case PRODUCTION_DC_ACTIVE_ENERGY: + case PRODUCTION_MAX_ACTIVE_POWER: + case PRODUCTION_MAX_AC_ACTIVE_POWER: + case PRODUCTION_MAX_DC_ACTUAL_POWER: + return Prediction24Hours.EMPTY; + + case CONSUMPTION_ACTIVE_POWER: + // TODO + return Prediction24Hours.EMPTY; + + case PRODUCTION_DC_ACTUAL_POWER: { + // Sum up "ActualPower" prediction of all EssDcChargers + List chargers = this.componentManager.getEnabledComponentsOfType(EssDcCharger.class); + Prediction24Hours[] predictions = new Prediction24Hours[chargers.size()]; + for (int i = 0; i < chargers.size(); i++) { + EssDcCharger charger = chargers.get(i); + predictions[i] = this.get24HoursPrediction( + new ChannelAddress(charger.id(), EssDcCharger.ChannelId.ACTUAL_POWER.id())); + } + return Prediction24Hours.sum(predictions); + } + case PRODUCTION_AC_ACTIVE_POWER: { + // Sum up "ActivePower" prediction of all SymmetricMeters + List meters = this.componentManager.getEnabledComponentsOfType(SymmetricMeter.class) + .stream() // + .filter(meter -> { + switch (meter.getMeterType()) { + case GRID: + case CONSUMPTION_METERED: + case CONSUMPTION_NOT_METERED: + return false; + case PRODUCTION: + case PRODUCTION_AND_CONSUMPTION: + // Get only Production meters + return true; + } + // should never come here + return false; + }).collect(Collectors.toList()); + Prediction24Hours[] predictions = new Prediction24Hours[meters.size()]; + for (int i = 0; i < meters.size(); i++) { + SymmetricMeter meter = meters.get(i); + predictions[i] = this.get24HoursPrediction( + new ChannelAddress(meter.id(), SymmetricMeter.ChannelId.ACTIVE_POWER.id())); + } + return Prediction24Hours.sum(predictions); + } + + case PRODUCTION_ACTIVE_POWER: + return Prediction24Hours.sum(// + this.getPredictionSum(Sum.ChannelId.PRODUCTION_AC_ACTIVE_POWER), // + this.getPredictionSum(Sum.ChannelId.PRODUCTION_DC_ACTUAL_POWER) // + ); + } + + // should never come here + return Prediction24Hours.EMPTY; + } + + /** + * Gets the best matching {@link Predictor24Hours} for the given + * {@link ChannelAddress}. + * + * @param channelAddress the {@link ChannelAddress} + * @return the {@link Predictor24Hours} - or null if none matches + */ + private synchronized Predictor24Hours getPredictorBestMatch(ChannelAddress channelAddress) { + int bestMatchValue = -1; + Predictor24Hours bestPredictor = null; + for (Predictor24Hours predictor : this.predictors) { + for (ChannelAddress pattern : predictor.getChannelAddresses()) { + int matchValue = ChannelAddress.match(channelAddress, pattern); + if (matchValue == 0) { + // Exact match + return predictor; + } else if (matchValue > bestMatchValue) { + bestMatchValue = matchValue; + bestPredictor = predictor; + } + } + } + return bestPredictor; + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/sum/SumImpl.java b/io.openems.edge.core/src/io/openems/edge/core/sum/SumImpl.java index caac3603044..8c599130a26 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/sum/SumImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/sum/SumImpl.java @@ -95,9 +95,9 @@ private void calculateChannelValues() { final CalculateIntegerSum essActivePowerL1 = new CalculateIntegerSum(); final CalculateIntegerSum essActivePowerL2 = new CalculateIntegerSum(); final CalculateIntegerSum essActivePowerL3 = new CalculateIntegerSum(); - + final CalculateIntegerSum essReactivePower = new CalculateIntegerSum(); - + final CalculateIntegerSum essMaxApparentPower = new CalculateIntegerSum(); final CalculateGridMode essGridMode = new CalculateGridMode(); final CalculateLongSum essActiveChargeEnergy = new CalculateLongSum(); @@ -267,10 +267,10 @@ private void calculateChannelValues() { this._setEssActivePowerL2(essActivePowerL2Sum); Integer essActivePowerL3Sum = essActivePowerL3.calculate(); this._setEssActivePowerL3(essActivePowerL3Sum); - + Integer essReactivePowerSum = essReactivePower.calculate(); this._setEssReactivePower(essReactivePowerSum); - + Integer essMaxApparentPowerSum = essMaxApparentPower.calculate(); this._setEssMaxApparentPower(essMaxApparentPowerSum); this._setGridMode(essGridMode.calculate()); diff --git a/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/data/InverterPrecision.java b/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/data/InverterPrecision.java index 0774d1e0cb6..ce74d5528c0 100644 --- a/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/data/InverterPrecision.java +++ b/io.openems.edge.ess.core/src/io/openems/edge/ess/core/power/data/InverterPrecision.java @@ -22,19 +22,19 @@ public class InverterPrecision { * Rounds each solution value to the Inverter precision; following this logic. * *

    - * On Discharge (Power > 0) + * On Discharge (Power > 0) * *

      - *
    • if SoC > 50 %: round up (more discharge) - *
    • if SoC < 50 %: round down (less discharge) + *
    • if SoC > 50 %: round up (more discharge) + *
    • if SoC < 50 %: round down (less discharge) *
    * *

    - * On Charge (Power < 0) + * On Charge (Power < 0) * *

      - *
    • if SoC > 50 %: round down (less charge) - *
    • if SoC < 50 %: round up (more discharge) + *
    • if SoC > 50 %: round down (less charge) + *
    • if SoC < 50 %: round up (more discharge) *
    * * @param coefficients the {@link Coefficients} diff --git a/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/AllowedChargeDischargeHandler.java b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/AllowedChargeDischargeHandler.java new file mode 100644 index 00000000000..0b121b7b5ca --- /dev/null +++ b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/AllowedChargeDischargeHandler.java @@ -0,0 +1,155 @@ +package io.openems.edge.ess.generic.symmetric; + +import java.time.Duration; +import java.time.Instant; +import java.util.function.BiConsumer; + +import io.openems.edge.battery.api.Battery; +import io.openems.edge.common.channel.value.Value; +import io.openems.edge.common.component.ClockProvider; +import io.openems.edge.common.startstop.StartStoppable; +import io.openems.edge.ess.api.ManagedSymmetricEss; + +/** + * Helper class to handle calculation of Allowed-Charge-Power and + * Allowed-Discharge-Power. This class is used by {@link ChannelManager} as a + * callback to updates of Battery Channels. + */ +public class AllowedChargeDischargeHandler implements BiConsumer { + + public final static float DISCHARGE_EFFICIENCY_FACTOR = 0.95F; + + /** + * Allow a maximum increase per second. + * + *

    + * 5 % of possible allowed charge/discharge power + */ + public final static float MAX_INCREASE_PERCENTAGE = 0.05F; + + private final ManagedSymmetricEss parent; + + public AllowedChargeDischargeHandler(ManagedSymmetricEss parent) { + this.parent = parent; + } + + protected float lastAllowedChargePower; + protected float lastAllowedDischargePower; + private Instant lastCalculate = null; + + @Override + public void accept(ClockProvider clockProvider, Battery battery) { + Value chargeMaxCurrent = battery.getChargeMaxCurrentChannel().getNextValue(); + Value dischargeMaxCurrent = battery.getDischargeMaxCurrentChannel().getNextValue(); + Value voltage = battery.getVoltageChannel().getNextValue(); + + final boolean isStarted; + if (this.parent instanceof StartStoppable) { + isStarted = ((StartStoppable) this.parent).isStarted(); + } else { + isStarted = true; + } + + this.calculateAllowedChargeDischargePower(clockProvider, isStarted, // + chargeMaxCurrent.get(), dischargeMaxCurrent.get(), voltage.get()); + + // Apply AllowedChargePower and AllowedDischargePower + this.parent._setAllowedChargePower(Math.round(this.lastAllowedChargePower * -1 /* invert charge power */)); + this.parent._setAllowedDischargePower(Math.round(this.lastAllowedDischargePower)); + } + + /** + * Calculates Allowed-Charge-Power and Allowed-Discharge Power from the given + * parameters. Result is stored in 'allowedChargePower' and + * 'allowedDischargePower' variables - both as positive values! + * + * @param isStarted is the ESS started? + * @param chargeMaxCurrent the {@link Battery.ChannelId#CHARGE_MAX_CURRENT} + * @param dischargeMaxCurrent the {@link Battery.ChannelId#DISHARGE_MAX_CURRENT} + * @param voltage the {@link Battery.ChannelId#VOLTAGE} + */ + protected void calculateAllowedChargeDischargePower(ClockProvider clockProvider, boolean isStarted, + Integer chargeMaxCurrent, Integer dischargeMaxCurrent, Integer voltage) { + final Instant now = Instant.now(clockProvider.getClock()); + float charge; + float discharge; + + /* + * Calculate initial AllowedChargePower and AllowedDischargePower + */ + if (!isStarted || chargeMaxCurrent == null || dischargeMaxCurrent == null || voltage == null) { + // Block ACTIVE and REACTIVE Power if + // - GenericEss is not in State "STARTED" + // - any of CHARGE_MAX_CURRENT, DISHARGE_MAX_CURRENT or VOLTAGE are missing + charge = 0; + discharge = 0; + + } else { + // Calculate AllowedChargePower and AllowedDischargePower from battery current + // limits and voltage. + // Efficiency factor is not considered in chargeMaxCurrent (DC Power > AC Power) + charge = chargeMaxCurrent * voltage; + discharge = Math.round(dischargeMaxCurrent * voltage * DISCHARGE_EFFICIENCY_FACTOR); + } + + /* + * Handle Force Charge and Discharge + */ + if (charge < 0 && discharge < 0) { + // Both Force Charge and Discharge are active -> cannot do anything + charge = 0; + discharge = 0; + + } else if (discharge < 0) { + // Force Charge is active + // Make sure AllowedChargePower is greater-or-equals absolute + // AllowedDischargePower + charge = Math.max(charge, Math.abs(discharge)); + + } else if (charge < 0) { + // Force Discharge is active + // Make sure AllowedDischargePower is greater-or-equals absolute + // AllowedChargePower + discharge = Math.max(Math.abs(charge), discharge); + } + + /* + * In Non-Force Mode: apply the max increase ramp. + */ + if (charge > 0) { + charge = applyMaxIncrease(this.lastAllowedChargePower, charge, this.lastCalculate, now); + } + if (discharge > 0) { + discharge = applyMaxIncrease(this.lastAllowedDischargePower, discharge, this.lastCalculate, now); + } + + /* + * Apply result + */ + this.lastCalculate = now; + this.lastAllowedChargePower = charge; + this.lastAllowedDischargePower = discharge; + } + + /** + * Applies the max increase ramp, built from MAX_INCREASE_PERCENTAGE. + * + * @param lastValue the result value in [W] of previous run + * @param thisValue the current value [W] + * @param lastInstant the timestamp of the previous run + * @param thisInstant the current timestamp + * @return the new value + */ + private static float applyMaxIncrease(float lastValue, float thisValue, Instant lastInstant, Instant thisInstant) { + final long millis; + if (lastValue < 0 || lastInstant == null) { + // was in Force-Mode before + lastValue = 0; + millis = 1000; + } else { + millis = Duration.between(lastInstant, thisInstant).toMillis(); + } + return Math.min(thisValue, // + lastValue + (thisValue * millis * MAX_INCREASE_PERCENTAGE) / 1000.F /* convert [mW] to [W] */); + } +} \ No newline at end of file diff --git a/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/ChannelManager.java b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/ChannelManager.java index 209ecc27853..fa47f02cefb 100644 --- a/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/ChannelManager.java +++ b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/ChannelManager.java @@ -1,14 +1,12 @@ package io.openems.edge.ess.generic.symmetric; -import java.util.function.Consumer; - import io.openems.edge.battery.api.Battery; import io.openems.edge.batteryinverter.api.ManagedSymmetricBatteryInverter; import io.openems.edge.batteryinverter.api.SymmetricBatteryInverter; import io.openems.edge.common.channel.AbstractChannelListenerManager; import io.openems.edge.common.channel.Channel; import io.openems.edge.common.channel.ChannelId; -import io.openems.edge.common.channel.value.Value; +import io.openems.edge.common.component.ClockProvider; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.ess.api.SymmetricEss; @@ -20,12 +18,11 @@ public class ChannelManager extends AbstractChannelListenerManager { private final GenericManagedSymmetricEss parent; - - private float lastAllowedChargePower = 0; - private float lastAllowedDischargePower = 0; + private final AllowedChargeDischargeHandler allowedChargeDischargeHandler; public ChannelManager(GenericManagedSymmetricEss parent) { this.parent = parent; + this.allowedChargeDischargeHandler = new AllowedChargeDischargeHandler(parent); } /** @@ -34,83 +31,19 @@ public ChannelManager(GenericManagedSymmetricEss parent) { * @param battery the {@link Battery} * @param batteryInverter the {@link ManagedSymmetricBatteryInverter} */ - public void activate(Battery battery, ManagedSymmetricBatteryInverter batteryInverter) { + public void activate(ClockProvider clockProvider, Battery battery, + ManagedSymmetricBatteryInverter batteryInverter) { /* * Battery */ - final Consumer> allowedChargeDischargeCallback = (value) -> { - // TODO: find proper efficiency factor to calculate AC Charge/Discharge limits - // from DC - final float efficiencyFactor = 0.95F; - - Value chargeMaxCurrent = battery.getChargeMaxCurrentChannel().getNextValue(); - Value dischargeMaxCurrent = battery.getDischargeMaxCurrentChannel().getNextValue(); - Value voltage = battery.getVoltageChannel().getNextValue(); - - float allowedChargePower; - float allowedDischargePower; - - /* - * Calculate initial AllowedChargePower and AllowedDischargePower - */ - if (!this.parent.isStarted()) { - // If the GenericEss is not in State "STARTED" block ACTIVE and REACTIVE Power! - allowedChargePower = 0; - allowedDischargePower = 0; - - } else { - // Calculate AllowedChargePower and AllowedDischargePower from battery current - // limits and voltage - // efficiency factor is not considered in chargeMaxCurrent (DC Power > AC Power) - allowedChargePower = chargeMaxCurrent.get() * voltage.get() * -1; - allowedDischargePower = dischargeMaxCurrent.get() * voltage.get() * efficiencyFactor; - } - - /* - * Allow max increase of 1 % per Call. - * - * NOTE: This code might be called multiple times per Cycle. - */ - if (allowedChargePower < 0 && this.lastAllowedChargePower < 0) { - allowedChargePower = Math.max(allowedChargePower, this.lastAllowedChargePower * 1.01F); - } - this.lastAllowedChargePower = allowedChargePower; - - if (allowedDischargePower > 0 && this.lastAllowedDischargePower > 0) { - allowedDischargePower = Math.min(allowedDischargePower, this.lastAllowedDischargePower * 1.01F); - } - this.lastAllowedDischargePower = allowedDischargePower; - - /* - * Handle Force Charge and Discharge - */ - if (allowedChargePower > 0 && allowedDischargePower < 0) { - // Both Force Charge and Discharge are active -> cannot do anything - allowedChargePower = 0; - allowedDischargePower = 0; - - } else if (allowedDischargePower < 0) { - // Force Charge is active - // Make sure AllowedChargePower is less-or-equals AllowedDischargePower - allowedChargePower = Math.min(allowedChargePower, allowedDischargePower); - - } else if (allowedChargePower > 0) { - // Force Discharge is active - // Make sure AllowedDischargePower is greater-or-equals AllowedChargePower - allowedDischargePower = Math.max(allowedChargePower, allowedDischargePower); - } - - // Apply AllowedChargePower and AllowedDischargePower - this.parent._setAllowedChargePower(Math.round(allowedChargePower)); - this.parent._setAllowedDischargePower(Math.round(allowedDischargePower)); - }; - this.addOnSetNextValueListener(battery, Battery.ChannelId.DISCHARGE_MIN_VOLTAGE, - allowedChargeDischargeCallback); + (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery)); this.addOnSetNextValueListener(battery, Battery.ChannelId.DISCHARGE_MAX_CURRENT, - allowedChargeDischargeCallback); - this.addOnSetNextValueListener(battery, Battery.ChannelId.CHARGE_MAX_VOLTAGE, allowedChargeDischargeCallback); - this.addOnSetNextValueListener(battery, Battery.ChannelId.CHARGE_MAX_CURRENT, allowedChargeDischargeCallback); + (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery)); + this.addOnSetNextValueListener(battery, Battery.ChannelId.CHARGE_MAX_VOLTAGE, + (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery)); + this.addOnSetNextValueListener(battery, Battery.ChannelId.CHARGE_MAX_CURRENT, + (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery)); this.addCopyListener(battery, // Battery.ChannelId.CAPACITY, // SymmetricEss.ChannelId.CAPACITY); diff --git a/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssImpl.java b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssImpl.java index ab87bc26b74..31d5fecd212 100644 --- a/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssImpl.java +++ b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssImpl.java @@ -28,6 +28,7 @@ import io.openems.edge.batteryinverter.api.ManagedSymmetricBatteryInverter; import io.openems.edge.batteryinverter.api.SymmetricBatteryInverter; 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.event.EdgeEventConstants; import io.openems.edge.common.modbusslave.ModbusSlave; @@ -60,6 +61,9 @@ public class GenericManagedSymmetricEssImpl extends AbstractOpenemsComponent imp @Reference private ConfigurationAdmin cm; + @Reference + private ComponentManager componentManager; + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) private ManagedSymmetricBatteryInverter batteryInverter; @@ -105,7 +109,7 @@ void activate(ComponentContext context, Config config) { return; } - this.channelHandler.activate(this.battery, this.batteryInverter); + this.channelHandler.activate(this.componentManager, this.battery, this.batteryInverter); this.config = config; } @@ -211,7 +215,7 @@ public Constraint[] getStaticConstraints() throws OpenemsNamedException { } private AtomicReference startStopTarget = new AtomicReference(StartStop.UNDEFINED); - + @Override public void setStartStop(StartStop value) { if (this.startStopTarget.getAndSet(value) != value) { @@ -247,5 +251,5 @@ public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { SymmetricEss.getModbusSlaveNatureTable(accessMode), // ManagedSymmetricEss.getModbusSlaveNatureTable(accessMode) // ); - } + } } diff --git a/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/package-info.java b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/package-info.java new file mode 100644 index 00000000000..ab8586d01ed --- /dev/null +++ b/io.openems.edge.ess.generic/src/io/openems/edge/ess/generic/symmetric/package-info.java @@ -0,0 +1,4 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +// TODO remove, once Ess-Sinexcel is migrated to Battery-Inverter; see #1389 +package io.openems.edge.ess.generic.symmetric; diff --git a/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/AllowedChargeDischargeHandlerTest.java b/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/AllowedChargeDischargeHandlerTest.java new file mode 100644 index 00000000000..db3b760264d --- /dev/null +++ b/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/AllowedChargeDischargeHandlerTest.java @@ -0,0 +1,90 @@ +package io.openems.edge.ess.generic.symmetric; + +import static org.junit.Assert.assertEquals; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; + +import org.junit.Test; + +import io.openems.edge.common.component.ClockProvider; +import io.openems.edge.common.test.ComponentTest; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.TimeLeapClock; + +public class AllowedChargeDischargeHandlerTest { + + @Test + public void testStart() throws Exception { + final GenericManagedSymmetricEssImpl ess = new GenericManagedSymmetricEssImpl(); + final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + final ClockProvider clockProvider = new DummyComponentManager(clock); + new ComponentTest(ess) // + .addReference("componentManager", clockProvider); // + + AllowedChargeDischargeHandler sut = new AllowedChargeDischargeHandler(ess); + + sut.calculateAllowedChargeDischargePower(clockProvider, false, null, null, null); + assertEquals(0, sut.lastAllowedChargePower, 0.001); + assertEquals(0, sut.lastAllowedDischargePower, 0.001); + clock.leap(1, ChronoUnit.SECONDS); + + sut.calculateAllowedChargeDischargePower(clockProvider, true, null, null, null); + assertEquals(0, sut.lastAllowedChargePower, 0.001); + assertEquals(0, sut.lastAllowedDischargePower, 0.001); + clock.leap(1, ChronoUnit.SECONDS); + + sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, -1, 500); + assertEquals(225, sut.lastAllowedChargePower, 0.001); + assertEquals(-475, sut.lastAllowedDischargePower, 0.001); + + clock.leap(250, ChronoUnit.MILLIS); + sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, -1, 500); + clock.leap(250, ChronoUnit.MILLIS); + sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, -1, 500); + assertEquals(-475, sut.lastAllowedDischargePower, 0.001); + clock.leap(250, ChronoUnit.MILLIS); + sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 0, 500); + clock.leap(250, ChronoUnit.MILLIS); + sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 0, 500); + assertEquals(450, sut.lastAllowedChargePower, 0.001); + assertEquals(0, sut.lastAllowedDischargePower, 0.001); + + clock.leap(1, ChronoUnit.SECONDS); + sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 0, 500); + assertEquals(675, sut.lastAllowedChargePower, 0.001); + assertEquals(0, sut.lastAllowedDischargePower, 0.001); + + for (int i = 0; i < 15; i++) { + clock.leap(1, ChronoUnit.SECONDS); + sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 1, 500); + } + + clock.leap(1, ChronoUnit.SECONDS); + sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 1, 500); + assertEquals(4275, sut.lastAllowedChargePower, 0.001); + assertEquals(380, sut.lastAllowedDischargePower, 0.001); + + clock.leap(1, ChronoUnit.SECONDS); + sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 1, 500); + assertEquals(4500, sut.lastAllowedChargePower, 0.001); + assertEquals(403.75, sut.lastAllowedDischargePower, 0.001); + + clock.leap(1, ChronoUnit.SECONDS); + sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 2, 500); + assertEquals(4500, sut.lastAllowedChargePower, 0.001); + assertEquals(451.25, sut.lastAllowedDischargePower, 0.001); + + clock.leap(1, ChronoUnit.SECONDS); + sut.calculateAllowedChargeDischargePower(clockProvider, true, 2, 0, 500); + assertEquals(1000, sut.lastAllowedChargePower, 0.001); + assertEquals(0, sut.lastAllowedDischargePower, 0.001); + + clock.leap(1, ChronoUnit.SECONDS); + sut.calculateAllowedChargeDischargePower(clockProvider, true, 9, 9, 500); + assertEquals(1225, sut.lastAllowedChargePower, 0.001); + assertEquals(213.75, sut.lastAllowedDischargePower, 0.001); + } + +} diff --git a/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssTest.java b/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssTest.java index bf33eddc9fc..263a0e5a969 100644 --- a/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssTest.java +++ b/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/GenericManagedSymmetricEssTest.java @@ -1,5 +1,8 @@ package io.openems.edge.ess.generic.symmetric; +import java.time.Instant; +import java.time.ZoneOffset; + import org.junit.Test; import io.openems.common.types.ChannelAddress; @@ -9,7 +12,9 @@ import io.openems.edge.common.startstop.StartStopConfig; import io.openems.edge.common.test.AbstractComponentTest.TestCase; 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.common.test.TimeLeapClock; import io.openems.edge.ess.generic.symmetric.statemachine.StateMachine.State; import io.openems.edge.ess.test.DummyPower; import io.openems.edge.ess.test.ManagedSymmetricEssTest; @@ -36,8 +41,10 @@ public class GenericManagedSymmetricEssTest { @Test public void testStart() throws Exception { + final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); new ComponentTest(new GenericManagedSymmetricEssImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager(clock)) // .addReference("batteryInverter", new DummyManagedSymmetricBatteryInverter(BATTERY_INVERTER_ID)) // .addReference("battery", new DummyBattery(BATTERY_ID)) // .activate(MyConfig.create() // @@ -66,6 +73,7 @@ public void testForceCharge() throws Exception { new ManagedSymmetricEssTest(new GenericManagedSymmetricEssImpl()) // .addReference("power", new DummyPower()) // .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", new DummyComponentManager()) // .addReference("batteryInverter", new DummyManagedSymmetricBatteryInverter(BATTERY_INVERTER_ID)) // .addReference("battery", new DummyBattery(BATTERY_ID) // .withVoltage(500) // diff --git a/io.openems.edge.ess.mr.gridcon/src/io/openems/edge/ess/mr/gridcon/Helper.java b/io.openems.edge.ess.mr.gridcon/src/io/openems/edge/ess/mr/gridcon/Helper.java index 9cb3d31689c..67b6650bb63 100644 --- a/io.openems.edge.ess.mr.gridcon/src/io/openems/edge/ess/mr/gridcon/Helper.java +++ b/io.openems.edge.ess.mr.gridcon/src/io/openems/edge/ess/mr/gridcon/Helper.java @@ -9,7 +9,8 @@ public class Helper { /** * Checks if all API values of a battery are set. - * @param Battery + * + * @param battery * @return true if all API values are filled */ public static boolean isUndefined(Battery battery) { diff --git a/io.openems.edge.ess.sinexcel/bnd.bnd b/io.openems.edge.ess.sinexcel/bnd.bnd index 0107f4c2c2d..b9ee370190a 100644 --- a/io.openems.edge.ess.sinexcel/bnd.bnd +++ b/io.openems.edge.ess.sinexcel/bnd.bnd @@ -10,6 +10,7 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.bridge.modbus,\ io.openems.edge.common,\ io.openems.edge.ess.api,\ + io.openems.edge.ess.generic,\ io.openems.edge.meter.api -testpath: \ diff --git a/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/ChannelManager.java b/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/ChannelManager.java index a6d804eb9b2..3eca32dab8c 100644 --- a/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/ChannelManager.java +++ b/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/ChannelManager.java @@ -2,13 +2,17 @@ import io.openems.edge.battery.api.Battery; import io.openems.edge.common.channel.AbstractChannelListenerManager; +import io.openems.edge.common.component.ClockProvider; +import io.openems.edge.ess.generic.symmetric.AllowedChargeDischargeHandler; public class ChannelManager extends AbstractChannelListenerManager { private final EssSinexcel parent; + private final AllowedChargeDischargeHandler allowedChargeDischargeHandler; public ChannelManager(EssSinexcel parent) { this.parent = parent; + this.allowedChargeDischargeHandler = new AllowedChargeDischargeHandler(parent); } /** @@ -16,7 +20,16 @@ public ChannelManager(EssSinexcel parent) { * * @param battery the {@link Battery} */ - public void activate(Battery battery) { + public void activate(ClockProvider clockProvider, Battery battery) { + this.addOnSetNextValueListener(battery, Battery.ChannelId.DISCHARGE_MIN_VOLTAGE, + (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery)); + this.addOnSetNextValueListener(battery, Battery.ChannelId.DISCHARGE_MAX_CURRENT, + (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery)); + this.addOnSetNextValueListener(battery, Battery.ChannelId.CHARGE_MAX_VOLTAGE, + (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery)); + this.addOnSetNextValueListener(battery, Battery.ChannelId.CHARGE_MAX_CURRENT, + (ignored) -> this.allowedChargeDischargeHandler.accept(clockProvider, battery)); + this.addOnChangeListener(battery, Battery.ChannelId.SOC, (oldValue, newValue) -> { this.parent._setSoc(newValue.get()); this.parent.channel(EssSinexcel.ChannelId.BAT_SOC).setNextValue(newValue.get()); diff --git a/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/EssSinexcelImpl.java b/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/EssSinexcelImpl.java index c9c7a9f7f2c..e1f67fe95df 100644 --- a/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/EssSinexcelImpl.java +++ b/io.openems.edge.ess.sinexcel/src/io/openems/edge/ess/sinexcel/EssSinexcelImpl.java @@ -27,6 +27,7 @@ import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; import io.openems.edge.bridge.modbus.api.ElementToChannelConverter; +import io.openems.edge.bridge.modbus.api.ElementToChannelConverterChain; import io.openems.edge.bridge.modbus.api.ModbusProtocol; import io.openems.edge.bridge.modbus.api.element.BitsWordElement; import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement; @@ -79,8 +80,6 @@ public class EssSinexcelImpl extends AbstractOpenemsModbusComponent protected int slowChargeVoltage = 4370; // for new batteries - 3940 protected int floatChargeVoltage = 4370; // for new batteries - 3940 - private int a = 0; - private int counterOn = 0; private int counterOff = 0; // State-Machines @@ -125,7 +124,7 @@ void activate(ComponentContext context, Config config) throws OpenemsNamedExcept if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "Battery", config.battery_id())) { return; } - this.channelHandler.activate(this.battery); + this.channelHandler.activate(this.componentManager, this.battery); this.slowChargeVoltage = config.toppingCharge(); this.floatChargeVoltage = config.toppingCharge(); @@ -158,21 +157,16 @@ public EssSinexcelImpl() throws OpenemsNamedException { private final static int MAX_CURRENT = 90; // [A] - private float lastAllowedChargePower = 0; - private float lastAllowedDischargePower = 0; - /** * Sets the Battery Ranges. Executed on TOPIC_CYCLE_AFTER_PROCESS_IMAGE. * * @throws OpenemsNamedException */ private void setBatteryRanges() throws OpenemsNamedException { - final float efficiencyFactor = 0.95F; final int disMaxA; final int chaMaxA; final int disMinV; final int chaMaxV; - final int voltage; // Evaluate input data if (battery == null) { @@ -180,13 +174,11 @@ private void setBatteryRanges() throws OpenemsNamedException { chaMaxA = 0; disMinV = 0; chaMaxV = 0; - voltage = 0; } else { disMaxA = battery.getDischargeMaxCurrent().orElse(0); chaMaxA = battery.getChargeMaxCurrent().orElse(0); disMinV = battery.getDischargeMinVoltage().orElse(0); chaMaxV = battery.getChargeMaxVoltage().orElse(0); - voltage = battery.getVoltage().orElse(0); } // Set Inverter Registers @@ -212,36 +204,6 @@ private void setBatteryRanges() throws OpenemsNamedException { IntegerWriteChannel chargeMaxVoltageChannel = this.channel(EssSinexcel.ChannelId.CHARGE_MAX_V); chargeMaxVoltageChannel.setNextWriteValue(chaMaxV * 10); } - - // Calculate AllowedCharge- and -DischargePower - float allowedChargePower; - float allowedDischargePower; - - // efficiency factor is not considered in chargeMaxCurrent (DC Power > AC Power) - allowedChargePower = chaMaxA * voltage * -1; - allowedDischargePower = disMaxA * voltage * efficiencyFactor; - - // Allow max increase of 1 % - if (allowedDischargePower > lastAllowedDischargePower + allowedDischargePower * 0.01F) { - allowedDischargePower = lastAllowedDischargePower + allowedDischargePower * 0.01F; - } - this.lastAllowedDischargePower = allowedDischargePower; - - if (allowedChargePower < lastAllowedChargePower + allowedChargePower * 0.01F) { - allowedChargePower = lastAllowedChargePower + allowedChargePower * 0.01F; - } - this.lastAllowedChargePower = allowedChargePower; - - // Make sure solution is feasible - if (allowedChargePower > allowedDischargePower) { // Force Discharge - allowedDischargePower = allowedChargePower; - } - if (allowedDischargePower < allowedChargePower) { // Force Charge - allowedChargePower = allowedDischargePower; - } - - this._setAllowedChargePower(Math.round(allowedChargePower)); - this._setAllowedDischargePower(Math.round(allowedDischargePower)); } /** @@ -457,7 +419,8 @@ protected ModbusProtocol defineModbusProtocol() throws OpenemsException { new FC3ReadRegistersTask(0x0248, Priority.HIGH, // m(SymmetricEss.ChannelId.ACTIVE_POWER, new SignedWordElement(0x0248), // - ElementToChannelConverter.SCALE_FACTOR_1), + new ElementToChannelConverterChain( + ElementToChannelConverter.SCALE_FACTOR_1, IGNORE_LESS_THAN_100)), new DummyRegisterElement(0x0249), m(EssSinexcel.ChannelId.FREQUENCY, new SignedWordElement(0x024A), ElementToChannelConverter.SCALE_FACTOR_MINUS_2), @@ -626,27 +589,16 @@ public void applyPower(int activePower, int reactivePower) throws OpenemsNamedEx IntegerWriteChannel setReactivePower = this.channel(EssSinexcel.ChannelId.SET_REACTIVE_POWER); setReactivePower.setNextWriteValue(reactivePower / 100); - if (this.stateOnOff() == false) { - a = 1; - } - - if (this.stateOnOff() == true) { - a = 0; - } - - if (activePower == 0 && reactivePower == 0 && a == 0) { + boolean isOn = this.stateOnOff(); + if (activePower == 0 && reactivePower == 0 && isOn) { this.counterOff++; if (this.counterOff == 48) { this.inverterOff(); this.counterOff = 0; } - } else if ((activePower != 0 || reactivePower != 0) && a == 1) { - this.counterOn++; - if (this.counterOn == 48) { - this.inverterOn(); - this.counterOn = 0; - } + } else if ((activePower != 0 || reactivePower != 0) && !isOn) { + this.inverterOn(); } break; @@ -706,4 +658,24 @@ public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { ManagedSymmetricEss.getModbusSlaveNatureTable(accessMode) // ); } + + /** + * The Sinexcel Battery Inverter claims to outputting a little bit of power even + * if it does not. This little filter ignores values for ActivePower less than + * 100 (charge/discharge). + */ + private static final ElementToChannelConverter IGNORE_LESS_THAN_100 = new ElementToChannelConverter(// + obj -> { + if (obj == null) { + return null; + } + int value = (Short) obj; + if (Math.abs(value) < 100) { + return 0; + } else { + return value; + } + }, // + value -> value); + } diff --git a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/ConfigSelfConsumption.java b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/ConfigSelfConsumption.java index f886150cf11..568153c2c58 100644 --- a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/ConfigSelfConsumption.java +++ b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/ConfigSelfConsumption.java @@ -28,9 +28,6 @@ @AttributeDefinition(name = "Evcs target filter", description = "This is auto-generated by 'Evcs-IDs'.") String Evcs_target() default ""; - @AttributeDefinition(name = "Ess-ID", description = "ID of Ess device.") - String ess_id() default "ess0"; - String webconsole_configurationFactory_nameHint() default "EVCS Cluster Self Consumption [{id}]"; } diff --git a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterPeakShaving.java b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterPeakShaving.java index 13aff2efcce..d3d4678aaaf 100644 --- a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterPeakShaving.java +++ b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterPeakShaving.java @@ -105,7 +105,7 @@ void activate(ComponentContext context, ConfigPeakShaving config) throws Openems this.config = config; - // update filter for 'evcss' component + // update filter for 'evcs' component if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "Evcs", config.evcs_ids())) { return; } diff --git a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterSelfConsumption.java b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterSelfConsumption.java index d293c997386..bdf92e8e478 100644 --- a/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterSelfConsumption.java +++ b/io.openems.edge.evcs.cluster/src/io/openems/edge/evcs/cluster/EvcsClusterSelfConsumption.java @@ -23,6 +23,7 @@ import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.event.Event; import org.osgi.service.event.EventConstants; +import org.osgi.service.event.EventHandler; import org.osgi.service.metatype.annotations.Designate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,7 +38,7 @@ EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_AFTER_CONTROLLERS, // EventConstants.EVENT_TOPIC + "=" + EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE // }) -public class EvcsClusterSelfConsumption extends AbstractEvcsCluster implements OpenemsComponent, Evcs { +public class EvcsClusterSelfConsumption extends AbstractEvcsCluster implements OpenemsComponent, Evcs, EventHandler { private final Logger log = LoggerFactory.getLogger(EvcsClusterSelfConsumption.class); diff --git a/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppServer.java b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppServer.java index c46dadde8a9..82c217d5dd9 100644 --- a/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppServer.java +++ b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppServer.java @@ -18,7 +18,7 @@ public interface OcppServer { * Example:

    * *
    -	 * send(session, request).whenComplete((confirmation, throwable) -> {
    +	 * send(session, request).whenComplete((confirmation, throwable) -> {
     	 * 	this.logInfo(log, confirmation.toString());
     	 * });
     	 * 
    diff --git a/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppStandardRequests.java b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppStandardRequests.java index 7d5a4aac521..4e44cf41d30 100644 --- a/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppStandardRequests.java +++ b/io.openems.edge.evcs.ocpp.common/src/io/openems/edge/evcs/ocpp/common/OcppStandardRequests.java @@ -12,7 +12,7 @@ public interface OcppStandardRequests { * Attention: The given power is given in watt. EVCS with the charging type AC * mostly send their limit values as amps. * - * @param ChargePower power that should be charged in watt. + * @param chargePower power that should be charged in watt. * @return Valid request that can be sent to the EVCS. */ Request setChargePowerLimit(int chargePower); diff --git a/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/MyJsonServer.java b/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/MyJsonServer.java index d223b56d9ce..5ae2c7d611c 100644 --- a/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/MyJsonServer.java +++ b/io.openems.edge.evcs.ocpp.server/src/io/openems/edge/evcs/ocpp/server/MyJsonServer.java @@ -170,7 +170,7 @@ public void sendDefault(UUID session, Request request) { * Sending initially all required requests to the EVCS. * * @param sessionIndex given session - * @param evcs given evcs + * @param ocppEvcs given evcs */ protected void sendInitialRequests(UUID sessionIndex, AbstractOcppEvcsComponent ocppEvcs) { // Setting the Evcss of this session id to available diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/ConsumptionHourlyPredictor.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/ConsumptionHourlyPredictor.java similarity index 52% rename from io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/ConsumptionHourlyPredictor.java rename to io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/ConsumptionHourlyPredictor.java index b65ef2e8068..7b5353f89fa 100644 --- a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/ConsumptionHourlyPredictor.java +++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/ConsumptionHourlyPredictor.java @@ -1,8 +1,9 @@ -package io.openems.edge.predictor.api; +package io.openems.edge.predictor.api.hourly; /** * Provides a consumption prediction for the next 24 h. */ +// TODO remove the ConsumptionHourlyPredictor in favor of PredictorManager API public interface ConsumptionHourlyPredictor extends HourlyPredictor { } diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/HourlyPrediction.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/HourlyPrediction.java similarity index 82% rename from io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/HourlyPrediction.java rename to io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/HourlyPrediction.java index 13d8cedc74a..099efcc2a1d 100644 --- a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/HourlyPrediction.java +++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/HourlyPrediction.java @@ -1,10 +1,11 @@ -package io.openems.edge.predictor.api; +package io.openems.edge.predictor.api.hourly; import java.time.ZonedDateTime; /** * Holds a prediction for 24 h; one value per hour; starting from 'start' time. */ +//TODO remove the HourlyPrediction in favor of PredictorManager API public class HourlyPrediction { private final Integer[] values = new Integer[24]; diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/HourlyPredictor.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/HourlyPredictor.java similarity index 81% rename from io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/HourlyPredictor.java rename to io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/HourlyPredictor.java index d2f5cff2d33..71087c9330a 100644 --- a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/HourlyPredictor.java +++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/HourlyPredictor.java @@ -1,4 +1,4 @@ -package io.openems.edge.predictor.api; +package io.openems.edge.predictor.api.hourly; import org.osgi.annotation.versioning.ProviderType; @@ -8,6 +8,7 @@ * Provides a prediction for the next 24 h. */ @ProviderType +//TODO remove the HourlyPredictor in favor of PredictorManager API public interface HourlyPredictor extends OpenemsComponent { /** diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/ProductionHourlyPredictor.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/ProductionHourlyPredictor.java similarity index 59% rename from io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/ProductionHourlyPredictor.java rename to io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/ProductionHourlyPredictor.java index 6a28eb17faa..e19ae0942b6 100644 --- a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/ProductionHourlyPredictor.java +++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/ProductionHourlyPredictor.java @@ -1,9 +1,10 @@ -package io.openems.edge.predictor.api; +package io.openems.edge.predictor.api.hourly; /** * Provides a production prediction for the next 24 h; e.g. for a photovoltaics * installation. */ +//TODO remove the ProductionHourlyPredictor in favor of PredictorManager API public interface ProductionHourlyPredictor extends HourlyPredictor { } diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/package-info.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/package-info.java new file mode 100644 index 00000000000..5bec3f92937 --- /dev/null +++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/hourly/package-info.java @@ -0,0 +1,4 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +//TODO remove in favor of PredictorManager API +package io.openems.edge.predictor.api.hourly; diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/manager/PredictorManager.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/manager/PredictorManager.java new file mode 100644 index 00000000000..ea8a49ebcda --- /dev/null +++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/manager/PredictorManager.java @@ -0,0 +1,33 @@ +package io.openems.edge.predictor.api.manager; + +import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.predictor.api.oneday.Prediction24Hours; +import io.openems.edge.predictor.api.oneday.Predictor24Hours; + +public interface PredictorManager extends OpenemsComponent { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + ; + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + public Doc doc() { + return this.doc; + } + } + + /** + * Gets the {@link Prediction24Hours} by the best matching + * {@link Predictor24Hours} for the given {@link ChannelAddress}. + * + * @param channelAddress the {@link ChannelAddress} + * @return the {@link Prediction24Hours} - all values null if no Predictor + * matches the Channel-Address + */ + public Prediction24Hours get24HoursPrediction(ChannelAddress channelAddress); +} diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/manager/package-info.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/manager/package-info.java new file mode 100644 index 00000000000..6dd9b2ce408 --- /dev/null +++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/manager/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.predictor.api.manager; diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/AbstractPredictor24Hours.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/AbstractPredictor24Hours.java new file mode 100644 index 00000000000..1a4ddd61c15 --- /dev/null +++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/AbstractPredictor24Hours.java @@ -0,0 +1,86 @@ +package io.openems.edge.predictor.api.oneday; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; + +import org.osgi.service.component.ComponentContext; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.component.ClockProvider; +import io.openems.edge.common.component.OpenemsComponent; + +public abstract class AbstractPredictor24Hours extends AbstractOpenemsComponent + implements Predictor24Hours, OpenemsComponent { + + protected static class PredictionContainer { + private Prediction24Hours latestPrediction = null; + private ZonedDateTime latestPredictionTimestamp = null; + } + + private final Map predictions = new HashMap<>(); + private ChannelAddress[] channelAddresses = new ChannelAddress[0]; + + protected abstract ClockProvider getClockProvider(); + + protected abstract Prediction24Hours createNewPrediction(ChannelAddress channelAddress); + + protected AbstractPredictor24Hours(io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds, + io.openems.edge.common.channel.ChannelId[]... furtherInitialChannelIds) { + super(firstInitialChannelIds, furtherInitialChannelIds); + } + + protected final void activate(ComponentContext context, String id, String alias, boolean enabled) { + throw new IllegalArgumentException("use the other activate method!"); + } + + protected void activate(ComponentContext context, String id, String alias, boolean enabled, + String[] channelAddresses) throws OpenemsNamedException { + super.activate(context, id, alias, enabled); + ChannelAddress[] channelAddressesArray = new ChannelAddress[channelAddresses.length]; + for (int i = 0; i < channelAddresses.length; i++) { + channelAddressesArray[i] = ChannelAddress.fromString(channelAddresses[i]); + } + this.channelAddresses = channelAddressesArray; + } + + @Override + public ChannelAddress[] getChannelAddresses() { + return this.channelAddresses; + } + + @Override + public Prediction24Hours get24HoursPrediction(ChannelAddress channelAddress) { + ZonedDateTime now = roundZonedDateTimeDownTo15Minutes(ZonedDateTime.now(this.getClockProvider().getClock())); + PredictionContainer container = this.predictions.get(channelAddress); + if (container == null) { + container = new PredictionContainer(); + this.predictions.put(channelAddress, container); + } + if (container.latestPredictionTimestamp == null || now.isAfter(container.latestPredictionTimestamp)) { + // Create new prediction + Prediction24Hours prediction = this.createNewPrediction(channelAddress); + container.latestPrediction = prediction; + container.latestPredictionTimestamp = now; + } else { + // Reuse existing prediction + } + return container.latestPrediction; + } + + /** + * Rounds a {@link ZonedDateTime} down to 15 minutes. + * + * @param d the {@link ZonedDateTime} + * @return the rounded result + */ + private static ZonedDateTime roundZonedDateTimeDownTo15Minutes(ZonedDateTime d) { + int minuteOfDay = d.get(ChronoField.MINUTE_OF_DAY); + return d.with(ChronoField.NANO_OF_DAY, 0).plus(minuteOfDay / 15 * 15, ChronoUnit.MINUTES); + } + +} diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/Prediction24Hours.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/Prediction24Hours.java new file mode 100644 index 00000000000..b435d771232 --- /dev/null +++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/Prediction24Hours.java @@ -0,0 +1,64 @@ +package io.openems.edge.predictor.api.oneday; + +import io.openems.edge.common.type.TypeUtils; + +/** + * Holds a prediction for 24 h; one value per 15 minutes; 96 values in total. + * + *

    + * Values have the same unit as the base Channel, i.e. if the Prediction relates + * to _sum/ProductionGridActivePower, the value is in unit Watt and represents + * the average Watt within a 15 minutes period. + */ +public class Prediction24Hours { + + public final static int NUMBER_OF_VALUES = 96; + + private final Integer[] values = new Integer[NUMBER_OF_VALUES]; + + /** + * Holds a {@link Prediction24Hours} with all values null. + */ + public final static Prediction24Hours EMPTY = new Prediction24Hours(new Integer[0]); + + /** + * Sums up the given {@link Prediction24Hours}s. If any source value is null, + * the result value is also null. + * + * @param predictions the given {@link Prediction24Hours} + * @return a {@link Prediction24Hours} holding the sum of all predictions. + */ + public static Prediction24Hours sum(Prediction24Hours... predictions) { + final Integer[] sumValues = new Integer[NUMBER_OF_VALUES]; + for (int i = 0; i < NUMBER_OF_VALUES; i++) { + Integer sumValue = null; + for (Prediction24Hours prediction : predictions) { + if (prediction.values[i] == null) { + sumValue = null; + break; + } else { + sumValue = TypeUtils.sum(sumValue, prediction.values[i]); + } + } + sumValues[i] = sumValue; + } + return new Prediction24Hours(sumValues); + } + + /** + * Constructs a {@link Prediction24Hours}. + * + * @param values the 96 prediction values + */ + public Prediction24Hours(Integer[] values) { + super(); + for (int i = 0; i < NUMBER_OF_VALUES && i < values.length; i++) { + this.values[i] = values[i]; + } + } + + public Integer[] getValues() { + return this.values; + } + +} diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/Predictor24Hours.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/Predictor24Hours.java new file mode 100644 index 00000000000..3478e4559bb --- /dev/null +++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/Predictor24Hours.java @@ -0,0 +1,39 @@ +package io.openems.edge.predictor.api.oneday; + +import org.osgi.annotation.versioning.ProviderType; + +import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.component.OpenemsComponent; + +/** + * Provides a prediction for the next 24 h; one value per 15 minutes; 96 values + * in total. + */ +@ProviderType +public interface Predictor24Hours extends OpenemsComponent { + + /** + * Gets the Channel-Addresses for which this Predictor can provide a prediction. + * + *

    + * The entries can contain wildcards to match multiple actual + * {@link ChannelAddress}es. + * + * @return an array of {@link ChannelAddress}es + */ + public ChannelAddress[] getChannelAddresses(); + + /** + * Gives a prediction for the next 24 h for the given {@link ChannelAddress}; + * one value per 15 minutes; 96 values in total. + * + *

    + * E.g. if called at 10:05, the first value stands for 10:00 to 10:15; second + * value for 10:15 to 10:30. + * + * @param channelAddress the {@link ChannelAddress} + * @return the {@link Prediction24Hours} + */ + public Prediction24Hours get24HoursPrediction(ChannelAddress channelAddress); + +} diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/package-info.java b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/package-info.java new file mode 100644 index 00000000000..2b584eb847b --- /dev/null +++ b/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/oneday/package-info.java @@ -0,0 +1,3 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +@org.osgi.annotation.bundle.Export +package io.openems.edge.predictor.api.oneday; diff --git a/io.openems.edge.predictor.persistencemodel/bnd.bnd b/io.openems.edge.predictor.persistencemodel/bnd.bnd index cbc64e94ea5..5fded39c511 100644 --- a/io.openems.edge.predictor.persistencemodel/bnd.bnd +++ b/io.openems.edge.predictor.persistencemodel/bnd.bnd @@ -1,15 +1,15 @@ -Bundle-Name: OpenEMS Edge Predictor Persistence Model +Bundle-Name: OpenEMS Edge Predictor Persistence-Model Bundle-Vendor: FENECON GmbH Bundle-License: https://opensource.org/licenses/EPL-2.0 Bundle-Version: 1.0.0.${tstamp} -Bundle-Description: \ - This Bundle describes the persistent model for predicting values. -buildpath: \ ${buildpath},\ io.openems.common,\ io.openems.edge.common,\ - io.openems.edge.predictor.api + io.openems.edge.controller.api,\ + io.openems.edge.predictor.api,\ + io.openems.edge.timedata.api -testpath: \ ${testpath} diff --git a/io.openems.edge.predictor.persistencemodel/readme.adoc b/io.openems.edge.predictor.persistencemodel/readme.adoc index 5f730cd320a..d95d7379713 100644 --- a/io.openems.edge.predictor.persistencemodel/readme.adoc +++ b/io.openems.edge.predictor.persistencemodel/readme.adoc @@ -1,3 +1,5 @@ -= Persistence Model Predictor += Persistence-Model Predictor -Predicts values using the 'same-as-last-day' approach. \ No newline at end of file +Predicts values using the 'same-as-last-day' approach. + +https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.predictor.holtwinters[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/AbstractPersistenceModelPredictor.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/AbstractPersistenceModelPredictor.java similarity index 93% rename from io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/AbstractPersistenceModelPredictor.java rename to io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/AbstractPersistenceModelPredictor.java index baec10c1de1..1498d66d2f5 100644 --- a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/AbstractPersistenceModelPredictor.java +++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/AbstractPersistenceModelPredictor.java @@ -1,4 +1,4 @@ -package io.openems.edge.predictor.persistencemodel; +package io.openems.edge.predictor.deprecatedpersistencemodel; import java.time.ZoneOffset; import java.time.ZonedDateTime; @@ -18,9 +18,10 @@ 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.predictor.api.HourlyPrediction; -import io.openems.edge.predictor.api.HourlyPredictor; +import io.openems.edge.predictor.api.hourly.HourlyPrediction; +import io.openems.edge.predictor.api.hourly.HourlyPredictor; +//TODO remove the AbstractPersistenceModelPredictor in favor of PredictorManager API public abstract class AbstractPersistenceModelPredictor extends AbstractOpenemsComponent implements HourlyPredictor { private final Logger log = LoggerFactory.getLogger(AbstractPersistenceModelPredictor.class); @@ -120,4 +121,4 @@ public HourlyPrediction get24hPrediction() { return hourlyPrediction; } -} +} \ No newline at end of file diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PredictorChannelId.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/PredictorChannelId.java similarity index 85% rename from io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PredictorChannelId.java rename to io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/PredictorChannelId.java index 54cd3f77ab6..ddac8dc056e 100644 --- a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PredictorChannelId.java +++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/PredictorChannelId.java @@ -1,4 +1,4 @@ -package io.openems.edge.predictor.persistencemodel; +package io.openems.edge.predictor.deprecatedpersistencemodel; import io.openems.common.channel.Level; import io.openems.edge.common.channel.Doc; diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/consumption/Config.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/consumption/Config.java similarity index 91% rename from io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/consumption/Config.java rename to io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/consumption/Config.java index 954a80d7023..c34d2214e3e 100644 --- a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/consumption/Config.java +++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/consumption/Config.java @@ -1,4 +1,4 @@ -package io.openems.edge.predictor.persistencemodel.consumption; +package io.openems.edge.predictor.deprecatedpersistencemodel.consumption; import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; @@ -18,4 +18,4 @@ boolean enabled() default true; String webconsole_configurationFactory_nameHint() default "Predictor Consumption Persistence-Model [{id}]"; -} +} \ No newline at end of file diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/consumption/ConsumptionPredictor.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/consumption/ConsumptionPredictor.java similarity index 88% rename from io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/consumption/ConsumptionPredictor.java rename to io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/consumption/ConsumptionPredictor.java index 7f2e86730cd..3c131bde9ad 100644 --- a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/consumption/ConsumptionPredictor.java +++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/consumption/ConsumptionPredictor.java @@ -1,4 +1,4 @@ -package io.openems.edge.predictor.persistencemodel.consumption; +package io.openems.edge.predictor.deprecatedpersistencemodel.consumption; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; @@ -16,8 +16,8 @@ import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.common.sum.Sum; -import io.openems.edge.predictor.api.ConsumptionHourlyPredictor; -import io.openems.edge.predictor.persistencemodel.AbstractPersistenceModelPredictor; +import io.openems.edge.predictor.api.hourly.ConsumptionHourlyPredictor; +import io.openems.edge.predictor.deprecatedpersistencemodel.AbstractPersistenceModelPredictor; @Designate(ocd = Config.class, factory = true) @Component(name = "Predictor.Consumption.PersistenceModel", // @@ -50,4 +50,4 @@ protected ComponentManager getComponentManager() { return this.componentManager; } -} +} \ No newline at end of file diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/production/Config.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/prediction/Config.java similarity index 91% rename from io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/production/Config.java rename to io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/prediction/Config.java index 86536d7e6a6..9bb4ed9246a 100644 --- a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/production/Config.java +++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/prediction/Config.java @@ -1,4 +1,4 @@ -package io.openems.edge.predictor.persistencemodel.production; +package io.openems.edge.predictor.deprecatedpersistencemodel.prediction; import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; @@ -18,4 +18,4 @@ boolean enabled() default true; String webconsole_configurationFactory_nameHint() default "Predictor Production Persistence-Model [{id}]"; -} +} \ No newline at end of file diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/production/ProductionPredictor.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/prediction/ProductionPredictor.java similarity index 88% rename from io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/production/ProductionPredictor.java rename to io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/prediction/ProductionPredictor.java index 59e9697c24d..ca5180835a2 100644 --- a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/production/ProductionPredictor.java +++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/deprecatedpersistencemodel/prediction/ProductionPredictor.java @@ -1,4 +1,4 @@ -package io.openems.edge.predictor.persistencemodel.production; +package io.openems.edge.predictor.deprecatedpersistencemodel.prediction; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; @@ -16,8 +16,8 @@ import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.common.sum.Sum; -import io.openems.edge.predictor.api.ProductionHourlyPredictor; -import io.openems.edge.predictor.persistencemodel.AbstractPersistenceModelPredictor; +import io.openems.edge.predictor.api.hourly.ProductionHourlyPredictor; +import io.openems.edge.predictor.deprecatedpersistencemodel.AbstractPersistenceModelPredictor; @Designate(ocd = Config.class, factory = true) @Component(name = "Predictor.Production.PersistenceModel", // @@ -49,4 +49,4 @@ protected void deactivate() { protected ComponentManager getComponentManager() { return this.componentManager; } -} +} \ No newline at end of file diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/Config.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/Config.java new file mode 100644 index 00000000000..3221812f9dc --- /dev/null +++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/Config.java @@ -0,0 +1,26 @@ +package io.openems.edge.predictor.persistencemodel; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(// + name = "Predictor Persistence-Model", // + description = "") +@interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "predictor0"; + + @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'") + // TODO "_sum/ConsumptionActivePower" holds also actively controlled consumption; replace, once we introduce a 'Sum-Non-Regulated-Consumption'-Channel + String[] channelAddresses() default { "_sum/ProductionActivePower", "_sum/ConsumptionActivePower" }; + + String webconsole_configurationFactory_nameHint() default "Predictor Persistence-Model [{id}]"; + +} \ No newline at end of file diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictor.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictor.java new file mode 100644 index 00000000000..c5fa7b04bf9 --- /dev/null +++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictor.java @@ -0,0 +1,24 @@ +package io.openems.edge.predictor.persistencemodel; + +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.predictor.api.oneday.Predictor24Hours; + +public interface PersistenceModelPredictor extends Predictor24Hours, 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.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictorImpl.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictorImpl.java new file mode 100644 index 00000000000..027e8677601 --- /dev/null +++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictorImpl.java @@ -0,0 +1,107 @@ +package io.openems.edge.predictor.persistencemodel; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.SortedMap; + +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.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.Sets; +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.ChannelAddress; +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.controller.api.Controller; +import io.openems.edge.predictor.api.oneday.AbstractPredictor24Hours; +import io.openems.edge.predictor.api.oneday.Prediction24Hours; +import io.openems.edge.predictor.api.oneday.Predictor24Hours; +import io.openems.edge.timedata.api.Timedata; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Predictor.PersistenceModel", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +public class PersistenceModelPredictorImpl extends AbstractPredictor24Hours + implements Predictor24Hours, OpenemsComponent { + + private final Logger log = LoggerFactory.getLogger(PersistenceModelPredictorImpl.class); + + @Reference + private Timedata timedata; + + @Reference + private ComponentManager componentManager; + + public PersistenceModelPredictorImpl() throws OpenemsNamedException { + super(// + OpenemsComponent.ChannelId.values(), // + Controller.ChannelId.values(), // + PersistenceModelPredictor.ChannelId.values() // + ); + } + + @Activate + protected void activate(ComponentContext context, Config config) throws OpenemsNamedException { + super.activate(context, config.id(), config.alias(), config.enabled(), config.channelAddresses()); + } + + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + protected Prediction24Hours createNewPrediction(ChannelAddress channelAddress) { + ZonedDateTime now = ZonedDateTime.now(this.componentManager.getClock()); + ZonedDateTime fromDate = now.minus(1, ChronoUnit.DAYS); + + // Query database + final SortedMap> queryResult; + try { + queryResult = this.timedata.queryHistoricData(null, fromDate, now, Sets.newHashSet(channelAddress), + 900 /* seconds per 15 minutes */); + } catch (OpenemsNamedException e) { + this.logError(this.log, e.getMessage()); + e.printStackTrace(); + return Prediction24Hours.EMPTY; + } + + // Extract data + Integer[] result = queryResult.values().stream() // + .map(m -> m.values()) // + // extract JsonElement values as flat stream + .flatMap(Collection::stream) // + // convert JsonElement to Integer + .map(v -> { + if (v.isJsonNull()) { + return (Integer) null; + } else { + return v.getAsInt(); + } + }) + // get as Array + .toArray(Integer[]::new); + + return new Prediction24Hours(result); + } + + @Override + protected ClockProvider getClockProvider() { + return this.componentManager; + } + +} diff --git a/io.openems.edge.predictor.persistencemodel/test/.gitignore b/io.openems.edge.predictor.persistencemodel/test/.gitignore index 90dde36e4ac..e69de29bb2d 100644 --- a/io.openems.edge.predictor.persistencemodel/test/.gitignore +++ b/io.openems.edge.predictor.persistencemodel/test/.gitignore @@ -1,3 +0,0 @@ -/bin/ -/bin_test/ -/generated/ diff --git a/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/production/MyConfig.java b/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/MyConfig.java similarity index 63% rename from io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/production/MyConfig.java rename to io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/MyConfig.java index 235aa4d43c7..2888f8200c1 100644 --- a/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/production/MyConfig.java +++ b/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/MyConfig.java @@ -1,12 +1,14 @@ -package io.openems.edge.predictor.persistencemodel.production; +package io.openems.edge.predictor.persistencemodel; import io.openems.edge.common.test.AbstractComponentConfig; +import io.openems.edge.predictor.persistencemodel.Config; @SuppressWarnings("all") public class MyConfig extends AbstractComponentConfig implements Config { protected static class Builder { private String id; + public String[] channelAddresses; private Builder() { } @@ -16,6 +18,11 @@ public Builder setId(String id) { return this; } + public Builder setChannelAddresses(String... channelAddresses) { + this.channelAddresses = channelAddresses; + return this; + } + public MyConfig build() { return new MyConfig(this); } @@ -37,4 +44,9 @@ private MyConfig(Builder builder) { this.builder = builder; } + @Override + public String[] channelAddresses() { + return this.builder.channelAddresses; + } + } \ No newline at end of file diff --git a/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictorTest.java b/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictorTest.java new file mode 100644 index 00000000000..9415e5b41b7 --- /dev/null +++ b/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/PersistenceModelPredictorTest.java @@ -0,0 +1,73 @@ +package io.openems.edge.predictor.persistencemodel; + +import static org.junit.Assert.assertEquals; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Arrays; + +import org.junit.Test; + +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.TimeLeapClock; +import io.openems.edge.predictor.api.oneday.Prediction24Hours; +import io.openems.edge.timedata.test.DummyTimedata; + +public class PersistenceModelPredictorTest { + + private static final String TIMEDATA_ID = "timedata0"; + private static final String PREDICTOR_ID = "predictor0"; + + private static final ChannelAddress METER1_ACTIVE_POWER = new ChannelAddress("meter1", "ActivePower"); + + @Test + public void test() throws Exception { + final TimeLeapClock clock = new TimeLeapClock( + Instant.ofEpochSecond(1577836800) /* starts at 1. January 2020 00:00:00 */, ZoneOffset.UTC); + int[] values = { + // Day 1 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 9, 146, 348, 636, 1192, 2092, 2882, 3181, + 3850, 5169, 6005, 6710, 7372, 8138, 8918, 9736, 10615, 11281, 11898, 12435, 11982, 14287, 15568, 16747, + 16934, 17221, 17573, 15065, 16726, 16670, 16696, 16477, 16750, 16991, 17132, 17567, 17003, 17686, 17753, + 17773, 17381, 17059, 17110, 16395, 15803, 15044, 14413, 13075, 12975, 6748, 7845, 10781, 8605, 6202, + 3049, 1697, 1184, 1142, 1015, 568, 1093, 414, 121, 110, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // Day 2 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 6, 146, 297, 489, 1111, 1953, 3825, + 2346, 3356, 3407, 3482, 4238, 7179, 11642, 5486, 4265, 5488, 5559, 6589, 7608, 9285, 7668, 6077, 3918, + 4498, 7221, 9628, 11962, 9483, 11746, 10401, 8875, 8825, 13945, 16488, 13038, 17702, 16772, 7319, 228, + 477, 501, 547, 589, 1067, 13304, 17367, 14825, 13654, 12545, 8371, 10468, 9810, 8537, 6228, 3758, 4131, + 3572, 1698, 1017, 569, 188, 14, 2, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + + DummyTimedata timedata = new DummyTimedata(TIMEDATA_ID); + ZonedDateTime start = ZonedDateTime.of(2019, 12, 30, 0, 0, 0, 0, ZoneId.of("UTC")); + for (int i = 0; i < values.length; i++) { + timedata.add(start.plusMinutes(i * 15), METER1_ACTIVE_POWER, values[i]); + } + + PersistenceModelPredictorImpl sut = new PersistenceModelPredictorImpl(); + + new ComponentTest(sut) // + .addReference("timedata", timedata) // + .addReference("componentManager", new DummyComponentManager(clock)) // + .activate(MyConfig.create() // + .setId(PREDICTOR_ID) // + .setChannelAddresses(METER1_ACTIVE_POWER.toString()) // + .build()); + + Prediction24Hours prediction = sut.get24HoursPrediction(METER1_ACTIVE_POWER); + Integer[] p = prediction.getValues(); + + assertEquals((Integer) 0, p[0]); + assertEquals((Integer) 3, p[20]); + assertEquals((Integer) 6, p[21]); + assertEquals((Integer) 146, p[22]); + assertEquals((Integer) 297, p[23]); + + System.out.println(Arrays.toString(prediction.getValues())); + } + +} diff --git a/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/production/ProductionPredictorTest.java b/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/production/ProductionPredictorTest.java deleted file mode 100644 index bf0214dcc03..00000000000 --- a/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/production/ProductionPredictorTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package io.openems.edge.predictor.persistencemodel.production; - -import static org.junit.Assert.assertEquals; - -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; - -import org.junit.Test; - -import io.openems.common.types.ChannelAddress; -import io.openems.edge.common.sum.DummySum; -import io.openems.edge.common.test.AbstractComponentTest.TestCase; -import io.openems.edge.common.test.ComponentTest; -import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; -import io.openems.edge.predictor.api.HourlyPrediction; - -public class ProductionPredictorTest { - - private static final String CTRL_ID = "ctrl0"; - - private static final String SUM_ID = "_sum"; - private static final ChannelAddress SUM_PRODUCTION_ACTIVE_ENERGY = new ChannelAddress(SUM_ID, - "ProductionActiveEnergy"); - - @Test - public void test() throws Exception { - final TimeLeapClock clock = new TimeLeapClock(); - final ProductionPredictor predictor = new ProductionPredictor(); - new ComponentTest(predictor) // - .addReference("componentManager", new DummyComponentManager(clock)) // - .addComponent(new DummySum()) // - .activate(MyConfig.create() // - .setId(CTRL_ID) // - .build()) - .next(new TestCase() // - .input(SUM_PRODUCTION_ACTIVE_ENERGY, 1000)) - .next(new TestCase() // - .timeleap(clock, 1, ChronoUnit.MINUTES) // - .input(SUM_PRODUCTION_ACTIVE_ENERGY, 1100)) - .next(new TestCase() // - .timeleap(clock, 1, ChronoUnit.HOURS) // - .input(SUM_PRODUCTION_ACTIVE_ENERGY, 2000)) - .next(new TestCase() // - .timeleap(clock, 1, ChronoUnit.HOURS) // - .input(SUM_PRODUCTION_ACTIVE_ENERGY, 4000)) - .next(new TestCase() // - .timeleap(clock, 1, ChronoUnit.HOURS) // - .input(SUM_PRODUCTION_ACTIVE_ENERGY, 5500)); - - HourlyPrediction p = predictor.get24hPrediction(); - assertEquals(p.getStart(), ZonedDateTime.now(clock).withNano(0).withMinute(0).withSecond(0)); - - Integer[] v = p.getValues(); - assertEquals(v.length, 24); - - assertEquals(null, v[0]); - assertEquals(null, v[1]); - assertEquals(null, v[2]); - assertEquals(null, v[3]); - assertEquals(null, v[4]); - assertEquals(null, v[5]); - assertEquals(null, v[6]); - assertEquals(null, v[7]); - assertEquals(null, v[8]); - assertEquals(null, v[9]); - assertEquals(null, v[10]); - assertEquals(null, v[11]); - assertEquals(null, v[12]); - assertEquals(null, v[13]); - assertEquals(null, v[14]); - assertEquals(null, v[15]); - assertEquals(null, v[16]); - assertEquals(null, v[17]); - assertEquals(null, v[18]); - assertEquals(null, v[19]); - assertEquals(null, v[20]); - assertEquals(Integer.valueOf(1000), v[21]); - assertEquals(Integer.valueOf(2000), v[22]); - assertEquals(Integer.valueOf(1500), v[23]); - } - -} diff --git a/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/Scheduler.java b/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/Scheduler.java index d2f1b55a587..a6109be9dcf 100644 --- a/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/Scheduler.java +++ b/io.openems.edge.scheduler.api/src/io/openems/edge/scheduler/api/Scheduler.java @@ -5,7 +5,6 @@ import org.osgi.annotation.versioning.ProviderType; import io.openems.common.channel.Level; -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.channel.StateChannel; import io.openems.edge.common.channel.value.Value; @@ -67,7 +66,6 @@ public default void _setControllerIsMissing(boolean value) { * {@link LinkedHashSet} is used, as it preserves insertion order * * @return a ordered set of Component-IDs of Controllers - * @throws OpenemsNamedException on error */ public LinkedHashSet getControllers(); diff --git a/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/DailySchedulerImplTest.java b/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/DailySchedulerImplTest.java index 54d1ea27ad9..a504b4f0668 100644 --- a/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/DailySchedulerImplTest.java +++ b/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/DailySchedulerImplTest.java @@ -34,7 +34,7 @@ public class DailySchedulerImplTest { public void test() throws Exception { final TimeLeapClock clock = new TimeLeapClock(Instant.parse("2020-01-01T00:00:00.00Z"), ZoneOffset.UTC); final DailyScheduler sut = new DailySchedulerImpl(); - ComponentTest test = new ComponentTest(sut) // + new ComponentTest(sut) // .addReference("componentManager", new DummyComponentManager(clock)) // .addComponent(new DummyController(CTRL0_ID)) // .addComponent(new DummyController(CTRL1_ID)) // @@ -58,24 +58,21 @@ public void test() throws Exception { .build()) // .build().toString()) .setAlwaysRunAfterControllerIds(CTRL3_ID, CTRL1_ID) // - .build()); // - - test.next(new TestCase("00:00")); // - assertEquals(// - Arrays.asList(CTRL2_ID, CTRL4_ID, CTRL3_ID, CTRL1_ID), // - getControllerIds(sut)); - - test.next(new TestCase("12:00") // - .timeleap(clock, 12, ChronoUnit.HOURS)); // - assertEquals(// - Arrays.asList(CTRL2_ID, CTRL0_ID, CTRL3_ID, CTRL1_ID), // - getControllerIds(sut)); - - test.next(new TestCase("14:00") // - .timeleap(clock, 12, ChronoUnit.HOURS)); // - assertEquals(// - Arrays.asList(CTRL2_ID, CTRL4_ID, CTRL3_ID, CTRL1_ID), // - getControllerIds(sut)); + .build()) // + .next(new TestCase("00:00") // + .onBeforeControllersCallbacks(() -> assertEquals(// + Arrays.asList(CTRL2_ID, CTRL4_ID, CTRL3_ID, CTRL1_ID), // + getControllerIds(sut)))) // + .next(new TestCase("12:00") // + .timeleap(clock, 12, ChronoUnit.HOURS) // + .onBeforeControllersCallbacks(() -> assertEquals(// + Arrays.asList(CTRL2_ID, CTRL0_ID, CTRL3_ID, CTRL1_ID), // + getControllerIds(sut)))) + .next(new TestCase("14:00") // + .timeleap(clock, 12, ChronoUnit.HOURS) // + .onBeforeControllersCallbacks(() -> assertEquals(// + Arrays.asList(CTRL2_ID, CTRL4_ID, CTRL3_ID, CTRL1_ID), // + getControllerIds(sut)))); } private static List getControllerIds(Scheduler scheduler) throws OpenemsNamedException { diff --git a/io.openems.edge.simulator/readme.adoc b/io.openems.edge.simulator/readme.adoc index ee25baf71a0..f09aacea8b8 100644 --- a/io.openems.edge.simulator/readme.adoc +++ b/io.openems.edge.simulator/readme.adoc @@ -9,21 +9,22 @@ The Simulator-App is a very specific component that needs to be handled with car CAUTION: Be aware that the SimulatorApp Component takes control over the complete OpenEMS Edge Application, i.e. if you enable it, it is going to *delete all existing Component configurations*! To run a simulation: -- Run OpenEMS Edge using the EdgeApp.bndrun -- Configure a read-write JSON/REST Api -- Send a https://openems.github.io/openems.io/openems/latest/edge/controller.html#_endpoint_jsonrpc[JSON-RPC Request] like the following, providing full configurations for all required OpenEMS Edge Components + +. Run OpenEMS Edge using the EdgeApp.bndrun +. Open up Apache Felix Web Console and + +.. activate a "Controller Api REST/JSON Read-Write" +.. activate a "Simulator App" + +. Send a https://openems.github.io/openems.io/openems/latest/edge/controller.html#_endpoint_jsonrpc[JSON-RPC Request] like the following, providing full configurations for all required OpenEMS Edge Components [source,json] ---- { - "jsonrpc":"2.0", - "id":"7132233f-1ca3-1eb3-8800-86d246d47c1d", "method":"componentJsonApi", "params":{ "componentId":"_simulator", "payload":{ - "jsonrpc":"2.0", - "id":"addccd39-1bac-89c2-91ac-13bf5b1e9743", "method":"executeSimulation", "params":{ "components":[ @@ -54,7 +55,7 @@ To run a simulation: }, { "name":"alias", - "value":"Verbrauch" + "value":"Consumption" }, { "name":"datasource.id", @@ -71,7 +72,7 @@ To run a simulation: }, { "name":"alias", - "value":"S�ddach" + "value":"South Roof" }, { "name":"datasource.id", diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorApp.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorApp.java index fc142898cae..ee7f4e5a84a 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorApp.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorApp.java @@ -12,6 +12,7 @@ import java.util.HashSet; import java.util.Hashtable; import java.util.List; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.SortedMap; @@ -331,7 +332,9 @@ private void deleteAllConfigurations(User user) throws OpenemsNamedException { if (factoryPid == null || factoryPid.trim().isEmpty()) { continue; } - if (factoryPid.startsWith("Core.") || factoryPid.startsWith("Controller.Api.")) { + if (factoryPid.startsWith("Core.") // + || factoryPid.startsWith("Controller.Api.") // + || factoryPid.startsWith("Predictor.")) { continue; } switch (factoryPid) { @@ -482,10 +485,31 @@ public SortedMap> queryHis ZonedDateTime fromDate, ZonedDateTime toDate, Set channels, int resolution) throws OpenemsNamedException { if (this.lastSimulation == null || this.lastSimulation.collectedData.isEmpty()) { + // return empty result return new TreeMap<>(); } + Period fakePeriod = this.convertToSimulatedFromToDates(fromDate, toDate); - return this.lastSimulation.collectedData.subMap(fakePeriod.fromDate, fakePeriod.toDate); + SortedMap> data = this.lastSimulation.collectedData + .subMap(fakePeriod.fromDate, fakePeriod.toDate); + + if (channels.isEmpty()) { + // No Channels given -> return all data + return data; + } + + SortedMap> result = new TreeMap<>(); + for (Entry> entry : this.lastSimulation.collectedData + .subMap(fakePeriod.fromDate, fakePeriod.toDate).entrySet()) { + SortedMap values = entry.getValue(); + TreeMap resultPerTimestamp = new TreeMap<>(); + for (ChannelAddress channel : channels) { + JsonElement value = values.get(channel); + resultPerTimestamp.put(channel, value == null ? JsonNull.INSTANCE : value); + } + result.put(entry.getKey(), resultPerTimestamp); + } + return result; } @Override diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/battery/BatteryDummy.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/battery/BatteryDummy.java index d6ee1d6eae0..faf571cce9b 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/battery/BatteryDummy.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/battery/BatteryDummy.java @@ -42,7 +42,8 @@ public class BatteryDummy extends AbstractOpenemsComponent public BatteryDummy() { super(// OpenemsComponent.ChannelId.values(), // - Battery.ChannelId.values() // + Battery.ChannelId.values(), // + StartStoppable.ChannelId.values() // ); } diff --git a/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2Core.java b/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2Core.java index 58cc34d7e07..a5de7409bd3 100644 --- a/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2Core.java +++ b/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2Core.java @@ -15,12 +15,12 @@ public interface TeslaPowerwall2Core extends OpenemsComponent { public Optional getBattery(); - public enum CoreChannelId implements io.openems.edge.common.channel.ChannelId { + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { SLAVE_COMMUNICATION_FAILED(Doc.of(Level.FAULT)); private final Doc doc; - private CoreChannelId(Doc doc) { + private ChannelId(Doc doc) { this.doc = doc; } @@ -36,7 +36,7 @@ public Doc doc() { * @return the Channel */ public default StateChannel getSlaveCommunicationFailedChannel() { - return this.channel(CoreChannelId.SLAVE_COMMUNICATION_FAILED); + return this.channel(ChannelId.SLAVE_COMMUNICATION_FAILED); } /** diff --git a/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2CoreImpl.java b/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2CoreImpl.java index 495447127c3..31d386569dc 100644 --- a/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2CoreImpl.java +++ b/io.openems.edge.tesla.powerwall2/src/io/openems/edge/tesla/powerwall2/core/TeslaPowerwall2CoreImpl.java @@ -40,7 +40,7 @@ public class TeslaPowerwall2CoreImpl extends AbstractOpenemsComponent public TeslaPowerwall2CoreImpl() { super(// OpenemsComponent.ChannelId.values(), // - CoreChannelId.values() // + TeslaPowerwall2Core.ChannelId.values() // ); } diff --git a/io.openems.edge.timedata.api/.settings/org.eclipse.core.resources.prefs b/io.openems.edge.timedata.api/.settings/org.eclipse.core.resources.prefs index ecd059818a9..fd04b997d93 100644 --- a/io.openems.edge.timedata.api/.settings/org.eclipse.core.resources.prefs +++ b/io.openems.edge.timedata.api/.settings/org.eclipse.core.resources.prefs @@ -1,5 +1,6 @@ eclipse.preferences.version=1 encoding//src/io/openems/edge/timedata/api/Timedata.java=UTF-8 encoding//src/io/openems/edge/timedata/api/package-info.java=UTF-8 +encoding//src/io/openems/edge/timedata/test/package-info.java=UTF-8 encoding//test/.gitignore=UTF-8 encoding/bnd.bnd=UTF-8 diff --git a/io.openems.edge.timedata.api/src/io/openems/edge/timedata/api/Timedata.java b/io.openems.edge.timedata.api/src/io/openems/edge/timedata/api/Timedata.java index eb48fdd9615..b4bc7737ff0 100644 --- a/io.openems.edge.timedata.api/src/io/openems/edge/timedata/api/Timedata.java +++ b/io.openems.edge.timedata.api/src/io/openems/edge/timedata/api/Timedata.java @@ -5,7 +5,6 @@ import org.osgi.annotation.versioning.ProviderType; -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.timedata.CommonTimedataService; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.channel.Doc; @@ -33,7 +32,6 @@ public Doc doc() { * * @param channelAddress the ChannelAddress to be queried * @return the latest known value or Empty - * @throws OpenemsNamedException on error */ public CompletableFuture> getLatestValue(ChannelAddress channelAddress); diff --git a/io.openems.edge.timedata.api/src/io/openems/edge/timedata/test/DummyTimedata.java b/io.openems.edge.timedata.api/src/io/openems/edge/timedata/test/DummyTimedata.java new file mode 100644 index 00000000000..0677c1908d3 --- /dev/null +++ b/io.openems.edge.timedata.api/src/io/openems/edge/timedata/test/DummyTimedata.java @@ -0,0 +1,105 @@ +package io.openems.edge.timedata.test; + +import java.time.ZonedDateTime; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.CompletableFuture; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + +import io.openems.common.exceptions.NotImplementedException; +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.channel.Channel; +import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.timedata.api.Timedata; + +/** + * Provides a simple, simulated {@link Timedata} component that can be used + * together with the OpenEMS Component test framework. + */ +public class DummyTimedata extends AbstractOpenemsComponent implements Timedata { + + private final SortedMap> data = new TreeMap<>(); + + public DummyTimedata(String id) { + super(// + OpenemsComponent.ChannelId.values(), // + Timedata.ChannelId.values() // + ); + for (Channel channel : this.channels()) { + channel.nextProcessImage(); + } + super.activate(null, id, "", true); + } + + /** + * Adds a value to the Dummy Timedata. + * + * @param timestamp the {@link ZonedDateTime} + * @param channelAddress the {@link ChannelAddress} + * @param value the value as {@link Integer} + */ + public void add(ZonedDateTime timestamp, ChannelAddress channelAddress, Integer value) { + this.add(timestamp, channelAddress, new JsonPrimitive(value)); + } + + /** + * Adds a value to the Dummy Timedata. + * + * @param timestamp the {@link ZonedDateTime} + * @param channelAddress the {@link ChannelAddress} + * @param value the value as {@link JsonElement} + */ + public void add(ZonedDateTime timestamp, ChannelAddress channelAddress, JsonElement value) { + SortedMap perTime = this.data.get(timestamp); + if (perTime == null) { + perTime = new TreeMap<>(); + this.data.put(timestamp, perTime); + } + perTime.put(channelAddress, value); + } + + @Override + public SortedMap> queryHistoricData(String edgeId, + ZonedDateTime fromDate, ZonedDateTime toDate, Set channels, int resolution) + throws OpenemsNamedException { + SortedMap> result = new TreeMap<>(); + for (Entry> entry : this.data.subMap(fromDate, toDate) + .entrySet()) { + SortedMap subResult = new TreeMap<>(); + for (ChannelAddress channelAddress : channels) { + subResult.put(channelAddress, entry.getValue().get(channelAddress)); + } + result.put(entry.getKey(), subResult); + } + return result; + } + + @Override + public SortedMap queryHistoricEnergy(String edgeId, ZonedDateTime fromDate, + ZonedDateTime toDate, Set channels) throws OpenemsNamedException { + // TODO Auto-generated method stub + throw new NotImplementedException("DummyTimedata.queryHistoricEnergy() is not implemented"); + } + + @Override + public SortedMap> queryHistoricEnergyPerPeriod(String edgeId, + ZonedDateTime fromDate, ZonedDateTime toDate, Set channels, int resolution) + throws OpenemsNamedException { + // TODO Auto-generated method stub + throw new NotImplementedException("DummyTimedata.queryHistoricEnergyPerPeriod() is not implemented"); + } + + @Override + public CompletableFuture> getLatestValue(ChannelAddress channelAddress) { + // TODO Auto-generated method stub + throw new IllegalArgumentException("DummyTimedata.getLatestValue() is not implemented"); + } + +} diff --git a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/package-info.java b/io.openems.edge.timedata.api/src/io/openems/edge/timedata/test/package-info.java similarity index 68% rename from io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/package-info.java rename to io.openems.edge.timedata.api/src/io/openems/edge/timedata/test/package-info.java index 5eae92ae19c..fc2211373df 100644 --- a/io.openems.edge.predictor.api/src/io/openems/edge/predictor/api/package-info.java +++ b/io.openems.edge.timedata.api/src/io/openems/edge/timedata/test/package-info.java @@ -1,3 +1,3 @@ @org.osgi.annotation.versioning.Version("1.0.0") @org.osgi.annotation.bundle.Export -package io.openems.edge.predictor.api; +package io.openems.edge.timedata.test; diff --git a/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jTimedataImpl.java b/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jTimedataImpl.java index 87094f854ab..6d32e546cdd 100644 --- a/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jTimedataImpl.java +++ b/io.openems.edge.timedata.rrd4j/src/io/openems/edge/timedata/rrd4j/Rrd4jTimedataImpl.java @@ -50,6 +50,7 @@ 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.timedata.api.Timedata; @Designate(ocd = Config.class, factory = true) @@ -60,10 +61,11 @@ public class Rrd4jTimedataImpl extends AbstractOpenemsComponent implements Rrd4jTimedata, Timedata, OpenemsComponent, EventHandler { + protected static final String DEFAULT_DATASOURCE_NAME = "value"; + protected static final int DEFAULT_STEP_SECONDS = 60; + protected static final int DEFAULT_HEARTBEAT_SECONDS = DEFAULT_STEP_SECONDS; + private static final String RRD4J_PATH = "rrd4j"; - private static final String DEFAULT_DATASOURCE_NAME = "value"; - private static final int DEFAULT_STEP_SECONDS = 60; - private static final int DEFAULT_HEARTBEAT_SECONDS = DEFAULT_STEP_SECONDS; private final Logger log = LoggerFactory.getLogger(Rrd4jTimedataImpl.class); @@ -112,6 +114,7 @@ public SortedMap> queryHis long toTimeStamp = toDate.withZoneSameInstant(ZoneOffset.UTC).toEpochSecond(); for (ChannelAddress channelAddress : channels) { + Channel channel = this.componentManager.getChannel(channelAddress); database = this.getExistingRrdDb(channel.address()); if (database == null) { @@ -121,23 +124,30 @@ public SortedMap> queryHis ChannelDef chDef = this.getDsDefForChannel(channel.channelDoc().getUnit()); FetchRequest request = database.createFetchRequest(chDef.consolFun, fromTimestamp, toTimeStamp, resolution); - FetchData data = request.fetchData(); + + // Post-Process data + double[] result = postProcessData(request, resolution); database.close(); - for (int i = 0; i < data.getTimestamps().length; i++) { - Instant timestampInstant = Instant.ofEpochSecond(data.getTimestamps()[i]); + for (int i = 0; i < result.length; i++) { + long timestamp = fromTimestamp + (i * resolution); + + // Prepare result table row + Instant timestampInstant = Instant.ofEpochSecond(timestamp); ZonedDateTime dateTime = ZonedDateTime.ofInstant(timestampInstant, ZoneOffset.UTC) .withZoneSameInstant(timezone); SortedMap tableRow = table.get(dateTime); if (tableRow == null) { tableRow = new TreeMap<>(); } - double value = data.getValues(0)[i]; + + double value = result[i]; if (Double.isNaN(value)) { tableRow.put(channelAddress, JsonNull.INSTANCE); } else { tableRow.put(channelAddress, new JsonPrimitive(value)); } + table.put(dateTime, tableRow); } } @@ -155,6 +165,77 @@ public SortedMap> queryHis return table; } + /** + * Post-Process the received data. + * + *

    + * This mainly makes sure the data has the correct resolution. + * + * @param request the RRD4j {@link FetchRequest} + * @param resolution the resolution in seconds + * @return the result array + * @throws IOException on error + * @throws IllegalArgumentException on error + */ + protected static double[] postProcessData(FetchRequest request, int resolution) + throws IOException, IllegalArgumentException { + FetchData data = request.fetchData(); + long step = data.getStep(); + double[] input = data.getValues()[0]; + + // Initialize result array + final double[] result = new double[(int) ((request.getFetchEnd() - request.getFetchStart()) / resolution)]; + for (int i = 0; i < result.length; i++) { + result[i] = Double.NaN; + } + + if (step < resolution) { + // Merge multiple entries to resolution + if (resolution % step != 0) { + throw new IllegalArgumentException( + "Requested resolution [" + resolution + "] is not dividable by RRD4j Step [" + step + "]"); + } + int merge = (int) (resolution / step); + double[] buffer = new double[merge]; + for (int i = 1; i < input.length; i += merge) { + for (int j = 0; j < merge; j++) { + if (i + j < input.length) { + buffer[j] = input[i + j]; + } else { + buffer[j] = Double.NaN; + } + } + + // put in result; avoid index rounding error + int resultIndex = (i - 1) / merge; + if (resultIndex >= result.length) { + break; + } + result[resultIndex] = TypeUtils.average(buffer); + } + + } else if (step > resolution) { + // Split each entry to multiple values + if (step % resolution != 0) { + throw new IllegalArgumentException( + "RRD4j Step [" + step + "] is not dividable by requested resolution [" + resolution + "]"); + } + int split = (int) (step / resolution); + for (int i = 1; i < input.length; i++) { + for (int j = 0; j < split; j++) { + result[(i - 1) * split + j] = input[i]; + } + } + + } else { + // Data already matches resolution + for (int i = 1; i < input.length; i++) { + result[i - 1] = input[i]; + } + } + return result; + } + @Override public SortedMap queryHistoricEnergy(String edgeId, ZonedDateTime fromDate, ZonedDateTime toDate, Set channels) throws OpenemsNamedException { diff --git a/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/Rrd4jTimedataImplTest.java b/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/Rrd4jTimedataImplTest.java new file mode 100644 index 00000000000..412b7f9fe5b --- /dev/null +++ b/io.openems.edge.timedata.rrd4j/test/io/openems/edge/timedata/rrd4j/Rrd4jTimedataImplTest.java @@ -0,0 +1,119 @@ +package io.openems.edge.timedata.rrd4j; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import org.junit.Test; +import org.rrd4j.ConsolFun; +import org.rrd4j.DsType; +import org.rrd4j.core.DsDef; +import org.rrd4j.core.FetchRequest; +import org.rrd4j.core.RrdDb; +import org.rrd4j.core.RrdDef; +import org.rrd4j.core.RrdMemoryBackendFactory; +import org.rrd4j.core.Sample; + +public class Rrd4jTimedataImplTest { + + private final static Instant START = Instant.ofEpochSecond(1577836800L); /* starts at 1. January 2020 00:00:00 */ + + private static void addSample(RrdDb database, Instant instant, double value) throws IOException { + Sample sample = database.createSample(instant.getEpochSecond()); + sample.setValue(0, value); + sample.update(); + } + + private static RrdDb createRrdDb(int oneMinute, int fiveMinutes) throws IOException, URISyntaxException { + final RrdDef rrdDef = new RrdDef("empty-path", START.getEpochSecond(), Rrd4jTimedataImpl.DEFAULT_STEP_SECONDS); + rrdDef.addDatasource(// + new DsDef(Rrd4jTimedataImpl.DEFAULT_DATASOURCE_NAME, // + DsType.GAUGE, // + Rrd4jTimedataImpl.DEFAULT_HEARTBEAT_SECONDS, // Heartbeat in [s], default 60 = 1 minute + Double.NaN, Double.NaN)); + // detailed recordings + rrdDef.addArchive(ConsolFun.AVERAGE, 0.5, 1, oneMinute); // 1 step (1 minute), 1440 rows (1 day) + rrdDef.addArchive(ConsolFun.AVERAGE, 0.5, 5, fiveMinutes); // 5 steps (5 minutes), 2880 rows (10 days) + // hourly values for a very long time + rrdDef.addArchive(ConsolFun.AVERAGE, 0.5, 60, 87_600); // 60 steps (1 hour), 87600 rows (10 years) + final RrdDb database = RrdDb.getBuilder() // + .setBackendFactory(new RrdMemoryBackendFactory()) // in memory + .setRrdDef(rrdDef) // + .build(); + + for (int i = 1; i <= 120; i++) { + addSample(database, START.plus(i, ChronoUnit.MINUTES), i); + } + return database; + } + + /** + * Test RRD4j step smaller than resolution. + * + * @throws IOException on error + * @throws URISyntaxException on error + */ + @Test + public void testMerge() throws IOException, URISyntaxException { + int resolution = 300; // 5 minutes + + RrdDb database = createRrdDb(1000, 2000); + FetchRequest request = database.createFetchRequest(ConsolFun.AVERAGE, START.getEpochSecond(), + START.plus(3, ChronoUnit.HOURS).getEpochSecond()); + double[] result = Rrd4jTimedataImpl.postProcessData(request, resolution); + database.close(); + + assertEquals(36, result.length); // 3 hours * 12 entries/per hour (5 minutes) = 36 + assertEquals(3.0, result[0], 0.001); + assertEquals(8.0, result[1], 0.001); + assertEquals(13.0, result[2], 0.001); + } + + /** + * Test RRD4j step equals resolution. + * + * @throws IOException on error + * @throws URISyntaxException on error + */ + @Test + public void testExact() throws IOException, URISyntaxException { + int resolution = 300; // 5 minutes + + RrdDb database = createRrdDb(10, 200); + FetchRequest request = database.createFetchRequest(ConsolFun.AVERAGE, START.getEpochSecond(), + START.plus(3, ChronoUnit.HOURS).getEpochSecond()); + double[] result = Rrd4jTimedataImpl.postProcessData(request, resolution); + database.close(); + + assertEquals(36, result.length); // 3 hours * 12 entries/per hour (5 minutes) = 36 + assertEquals(3.0, result[0], 0.001); + assertEquals(8.0, result[1], 0.001); + assertEquals(13.0, result[2], 0.001); + } + + /** + * Test RRD4j step bigger than resolution. + * + * @throws IOException on error + * @throws URISyntaxException on error + */ + @Test + public void testSplit() throws IOException, URISyntaxException { + int resolution = 300; // 5 minutes + + RrdDb database = createRrdDb(10, 20); + FetchRequest request = database.createFetchRequest(ConsolFun.AVERAGE, START.getEpochSecond(), + START.plus(3, ChronoUnit.HOURS).getEpochSecond()); + double[] result = Rrd4jTimedataImpl.postProcessData(request, resolution); + database.close(); + + assertEquals(36, result.length); // 3 hours * 12 entries/per hour (5 minutes) = 36 + assertEquals(30.5, result[0], 0.001); + assertEquals(30.5, result[1], 0.001); + assertEquals(30.5, result[2], 0.001); + assertEquals(90.5, result[12], 0.001); + } +} diff --git a/io.openems.shared.influxdb/src/io/openems/shared/influxdb/InfluxConnector.java b/io.openems.shared.influxdb/src/io/openems/shared/influxdb/InfluxConnector.java index ad9506a409b..2d8f08f5f16 100644 --- a/io.openems.shared.influxdb/src/io/openems/shared/influxdb/InfluxConnector.java +++ b/io.openems.shared.influxdb/src/io/openems/shared/influxdb/InfluxConnector.java @@ -65,7 +65,7 @@ public class InfluxConnector { * @param isReadOnly If true, a 'Read-Only-Mode' is activated, where no data * is actually written to the database * @param onWriteError A callback for write-errors, i.e. '(failedPoints, - * throwable) -> {}' + * throwable) -> {}' */ public InfluxConnector(String ip, int port, String username, String password, String database, String retentionPolicy, boolean isReadOnly, BiConsumer, Throwable> onWriteError) { diff --git a/ui/package-lock.json b/ui/package-lock.json index c60cdbd2bdb..9a66636b75d 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "openems-ui", - "version": "2021.3.0", + "version": "2021.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3039,25 +3039,25 @@ } }, "@ionic-native/core": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@ionic-native/core/-/core-5.30.0.tgz", - "integrity": "sha512-UkktFoSOAt/lgsc1nxnwjCul29yD06qHNjyv7/K7JxhqeJrqPBKihnkLu7OTAe52KdFBozRxLKDP6HWcGderqA==", + "version": "5.31.1", + "resolved": "https://registry.npmjs.org/@ionic-native/core/-/core-5.31.1.tgz", + "integrity": "sha512-dbJHezSuY8OqyFwyQiS+5QscA/BONhWitXgniljEblC5kQeLOCe+8p30JYHXj9xDciYzfqFP8ICmyaGOqUHJYw==", "requires": { "@types/cordova": "^0.0.34" } }, "@ionic-native/splash-screen": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@ionic-native/splash-screen/-/splash-screen-5.30.0.tgz", - "integrity": "sha512-QlVPuPqJeb4fkxEJ2M0tgUxcjSOPGDLqrFtgnosun6ZuYF8RBlnNu79/h9pa0jcTpq83U53zng/T5qw0M/ccTA==", + "version": "5.31.1", + "resolved": "https://registry.npmjs.org/@ionic-native/splash-screen/-/splash-screen-5.31.1.tgz", + "integrity": "sha512-Hcy1cMjWLnFE0TrIhpcNwld39dFipOQE63XpKuEhSJXfix1hibrC+0Nc3jEn0zBJUbbAHVJph6s9dohUxRycqg==", "requires": { "@types/cordova": "^0.0.34" } }, "@ionic-native/status-bar": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@ionic-native/status-bar/-/status-bar-5.30.0.tgz", - "integrity": "sha512-AQglp5M5E3QN/aA3ERl76Fajpb1G2Zvnk4sJOo4ra5uRlw2lZ6E36DdpNo/NO/7ALxaddD9qXO/LCmvU72Obsg==", + "version": "5.31.1", + "resolved": "https://registry.npmjs.org/@ionic-native/status-bar/-/status-bar-5.31.1.tgz", + "integrity": "sha512-o5gugiuyYjWqQqzajbfqYKvuWX0BSTppwoLXU0PidgvBWtw3yBb7z4FZoo6JQSkUVn2AWYD1XEj/4KKh9D4Pkg==", "requires": { "@types/cordova": "^0.0.34" } @@ -3133,17 +3133,17 @@ } }, "@ngx-formly/core": { - "version": "5.10.13", - "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-5.10.13.tgz", - "integrity": "sha512-WDKCK7wLrvp49FXcql8iZHC2wSgPbp3bk5w5yc1LgZpLmk+ilPoquHPT+/kYM/ctxkQFwcrO78KiKMMEfz5Ytg==", + "version": "5.10.14", + "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-5.10.14.tgz", + "integrity": "sha512-8ZVIxte3GGQyhBZGSFHF8ieZ3CO8lT4XADi37lz1/yjwbA09/1BcehqXQpA3P0CD+jnKx2WiDpMQIasU4SiqrA==", "requires": { "tslib": "^1.7.1" } }, "@ngx-formly/ionic": { - "version": "5.10.13", - "resolved": "https://registry.npmjs.org/@ngx-formly/ionic/-/ionic-5.10.13.tgz", - "integrity": "sha512-vWZr+bXb8cuG5Ld/DZ33ZyVinoY/Ks2OGA0I/rlxYbkoi6kPvWWlfGqv0EolAtNPQFZPalBekIuG76WUtVMjaA==", + "version": "5.10.14", + "resolved": "https://registry.npmjs.org/@ngx-formly/ionic/-/ionic-5.10.14.tgz", + "integrity": "sha512-goLD9f793BCmWDONjgV5XBZWczO6yM9xzUvvw9WsZXNj304RZDSacCjkzk3yuGxhhXTNhR3K7gBChRu0+dVdEQ==", "requires": { "tslib": "^1.9.0" } @@ -4384,15 +4384,6 @@ "tweetnacl": "^0.14.3" } }, - "better-assert": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", - "dev": true, - "requires": { - "callsite": "1.0.0" - } - }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -4828,12 +4819,6 @@ "caller-callsite": "^2.0.0" } }, - "callsite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", - "dev": true - }, "callsites": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", @@ -5642,9 +5627,9 @@ } }, "core-js": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.2.tgz", - "integrity": "sha512-FfApuSRgrR6G5s58casCBd9M2k+4ikuu4wbW6pJyYU7bd9zvFc9qf7vr5xmrZOhT9nn+8uwlH1oRR9jTnFoA3A==" + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.9.0.tgz", + "integrity": "sha512-PyFBJaLq93FlyYdsndE5VaueA9K5cNB7CGzeCj191YYLhkQM0gdZR2SKihM70oF0wdqKSKClv/tEBOpoRmdOVQ==" }, "core-js-compat": { "version": "3.8.3", @@ -6851,37 +6836,37 @@ } }, "engine.io": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz", - "integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz", + "integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==", "dev": true, "requires": { "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "0.3.1", + "cookie": "~0.4.1", "debug": "~4.1.0", "engine.io-parser": "~2.2.0", - "ws": "^7.1.2" + "ws": "~7.4.2" }, "dependencies": { "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", "dev": true }, "ws": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", - "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.3.tgz", + "integrity": "sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==", "dev": true } } }, "engine.io-client": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.4.tgz", - "integrity": "sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.0.tgz", + "integrity": "sha512-12wPRfMrugVw/DNyJk34GQ5vIVArEcVMXWugQGGuw2XxUSztFNmJggZmv8IZlLyEdnpO1QB9LkcjeWewO2vxtA==", "dev": true, "requires": { "component-emitter": "~1.3.0", @@ -6892,7 +6877,7 @@ "indexof": "0.0.1", "parseqs": "0.0.6", "parseuri": "0.0.6", - "ws": "~6.1.0", + "ws": "~7.4.2", "xmlhttprequest-ssl": "~1.5.4", "yeast": "0.1.2" }, @@ -6912,26 +6897,11 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, - "parseqs": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", - "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==", - "dev": true - }, - "parseuri": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", - "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==", - "dev": true - }, "ws": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", - "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.3.tgz", + "integrity": "sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==", + "dev": true } } }, @@ -9844,7 +9814,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "requires": { "graceful-fs": "^4.1.2", @@ -11131,12 +11101,6 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, - "object-component": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", - "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", - "dev": true - }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -11727,22 +11691,16 @@ } }, "parseqs": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", - "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==", + "dev": true }, "parseuri": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", - "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==", + "dev": true }, "parseurl": { "version": "1.3.3", @@ -13641,9 +13599,9 @@ "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" }, "rxjs": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", - "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.6.tgz", + "integrity": "sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==", "requires": { "tslib": "^1.9.0" } @@ -14182,16 +14140,16 @@ } }, "socket.io": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", - "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.4.1.tgz", + "integrity": "sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==", "dev": true, "requires": { "debug": "~4.1.0", - "engine.io": "~3.4.0", + "engine.io": "~3.5.0", "has-binary2": "~1.0.2", "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.3.0", + "socket.io-client": "2.4.0", "socket.io-parser": "~3.4.0" } }, @@ -14202,38 +14160,32 @@ "dev": true }, "socket.io-client": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", - "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz", + "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==", "dev": true, "requires": { "backo2": "1.0.2", - "base64-arraybuffer": "0.1.5", "component-bind": "1.0.0", - "component-emitter": "1.2.1", - "debug": "~4.1.0", - "engine.io-client": "~3.4.0", + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "engine.io-client": "~3.5.0", "has-binary2": "~1.0.2", - "has-cors": "1.1.0", "indexof": "0.0.1", - "object-component": "0.0.3", - "parseqs": "0.0.5", - "parseuri": "0.0.5", + "parseqs": "0.0.6", + "parseuri": "0.0.6", "socket.io-parser": "~3.3.0", "to-array": "0.1.4" }, "dependencies": { - "base64-arraybuffer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", - "dev": true - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } }, "isarray": { "version": "2.0.1", @@ -14248,31 +14200,14 @@ "dev": true }, "socket.io-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz", - "integrity": "sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz", + "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==", "dev": true, "requires": { "component-emitter": "~1.3.0", "debug": "~3.1.0", "isarray": "2.0.1" - }, - "dependencies": { - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } } } } diff --git a/ui/package.json b/ui/package.json index 40a51583faf..9702a6f10af 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "openems-ui", - "version": "2021.3.0", + "version": "2021.4.0", "author": "OpenEMS Association e.V.", "homepage": "http://openems.io", "scripts": { @@ -23,9 +23,9 @@ "@angular/platform-browser-dynamic": "^10.2.4", "@angular/router": "^10.2.4", "@angular/service-worker": "^10.2.4", - "@ionic-native/core": "^5.30.0", - "@ionic-native/splash-screen": "^5.30.0", - "@ionic-native/status-bar": "^5.30.0", + "@ionic-native/core": "^5.31.1", + "@ionic-native/splash-screen": "^5.31.1", + "@ionic-native/status-bar": "^5.31.1", "@ionic/angular": "^5.5.4", "@ngx-formly/core": "^5.10.13", "@ngx-formly/ionic": "^5.10.13", @@ -34,7 +34,7 @@ "angular2-uuid": "^1.1.1", "chart.js": "^2.9.4", "classlist.js": "^1.1.20150312", - "core-js": "^3.8.2", + "core-js": "^3.9.0", "d3": "5.15.0", "date-fns": "^2.17.0", "file-saver": "^2.0.5", @@ -44,7 +44,7 @@ "ngx-spinner": "^10.0.1", "node-sass": "^4.14.1", "roboto-fontface": "0.10.0", - "rxjs": "^6.6.3", + "rxjs": "^6.6.6", "semver-compare-multi": "^1.0.3", "tslib": "^1.14.1", "zone.js": "^0.10.3" diff --git a/ui/src/app/about/about.component.html b/ui/src/app/about/about.component.html index 69beb34db08..bda39c0d3fc 100644 --- a/ui/src/app/about/about.component.html +++ b/ui/src/app/about/about.component.html @@ -19,8 +19,8 @@ About.openEMS

  • - - About.build: 2021.3.0 (2021-02-11) + + About.build: 2021.4.0 (2021-02-26)
  • diff --git a/ui/src/app/edge/history/abstracthistorychart.ts b/ui/src/app/edge/history/abstracthistorychart.ts index e247e9266b0..4baf315706d 100644 --- a/ui/src/app/edge/history/abstracthistorychart.ts +++ b/ui/src/app/edge/history/abstracthistorychart.ts @@ -1,13 +1,13 @@ -import { ChannelAddress, Edge, EdgeConfig, Service, Utils } from "../../shared/shared"; +import { TranslateService } from '@ngx-translate/core'; import { ChartDataSets } from 'chart.js'; -import { ChartOptions, DEFAULT_TIME_CHART_OPTIONS, EMPTY_DATASET } from './shared'; import { differenceInDays } from 'date-fns'; +import { queryHistoricTimeseriesEnergyPerPeriodRequest } from 'src/app/shared/jsonrpc/request/queryHistoricTimeseriesEnergyPerPeriodRequest'; +import { queryHistoricTimeseriesEnergyPerPeriodResponse } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse'; import { JsonrpcResponseError } from "../../shared/jsonrpc/base"; import { QueryHistoricTimeseriesDataRequest } from "../../shared/jsonrpc/request/queryHistoricTimeseriesDataRequest"; import { QueryHistoricTimeseriesDataResponse } from "../../shared/jsonrpc/response/queryHistoricTimeseriesDataResponse"; -import { queryHistoricTimeseriesEnergyPerPeriodRequest } from 'src/app/shared/jsonrpc/request/queryHistoricTimeseriesEnergyPerPeriodRequest'; -import { queryHistoricTimeseriesEnergyPerPeriodResponse } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse'; -import { TranslateService } from '@ngx-translate/core'; +import { ChannelAddress, Edge, EdgeConfig, Service, Utils } from "../../shared/shared"; +import { ChartOptions, DEFAULT_TIME_CHART_OPTIONS, EMPTY_DATASET } from './shared'; // NOTE: Auto-refresh of widgets is currently disabled to reduce server load export abstract class AbstractHistoryChart { diff --git a/ui/src/app/edge/history/energy/energy.component.ts b/ui/src/app/edge/history/energy/energy.component.ts index 03c32218c8d..8d422158d03 100644 --- a/ui/src/app/edge/history/energy/energy.component.ts +++ b/ui/src/app/edge/history/energy/energy.component.ts @@ -1,25 +1,25 @@ -import { AbstractHistoryChart } from '../abstracthistorychart'; +import { formatNumber } from '@angular/common'; +import { Component, Input, OnChanges } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { addDays } from 'date-fns/esm'; -import { Base64PayloadResponse } from 'src/app/shared/jsonrpc/response/base64PayloadResponse'; -import { ChannelAddress, Edge, EdgeConfig, Service, Utils, Websocket } from '../../../shared/shared'; +import { ModalController, Platform } from '@ionic/angular'; +import { TranslateService } from '@ngx-translate/core'; import { ChartData, ChartDataSets, ChartLegendLabelItem, ChartTooltipItem } from 'chart.js'; -import { ChartOptions, Data, DEFAULT_TIME_CHART_OPTIONS, TooltipItem } from './../shared'; -import { Component, Input, OnChanges } from '@angular/core'; -import { DefaultTypes } from 'src/app/shared/service/defaulttypes'; import { differenceInDays, format, isSameDay, isSameMonth, isSameYear } from 'date-fns'; -import { EnergyModalComponent } from './modal/modal.component'; -import { formatNumber } from '@angular/common'; -import { ModalController, Platform } from '@ionic/angular'; -import { QueryHistoricTimeseriesDataResponse } from '../../../shared/jsonrpc/response/queryHistoricTimeseriesDataResponse'; -import { queryHistoricTimeseriesEnergyPerPeriodResponse } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse'; -import { QueryHistoricTimeseriesEnergyResponse } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyResponse'; -import { QueryHistoricTimeseriesExportXlxsRequest } from 'src/app/shared/jsonrpc/request/queryHistoricTimeseriesExportXlxs'; +import { addDays } from 'date-fns/esm'; +import * as FileSaver from 'file-saver'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { TranslateService } from '@ngx-translate/core'; +import { QueryHistoricTimeseriesExportXlxsRequest } from 'src/app/shared/jsonrpc/request/queryHistoricTimeseriesExportXlxs'; +import { Base64PayloadResponse } from 'src/app/shared/jsonrpc/response/base64PayloadResponse'; +import { queryHistoricTimeseriesEnergyPerPeriodResponse } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse'; +import { QueryHistoricTimeseriesEnergyResponse } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyResponse'; import { UnitvaluePipe } from 'src/app/shared/pipe/unitvalue/unitvalue.pipe'; -import * as FileSaver from 'file-saver'; +import { DefaultTypes } from 'src/app/shared/service/defaulttypes'; +import { QueryHistoricTimeseriesDataResponse } from '../../../shared/jsonrpc/response/queryHistoricTimeseriesDataResponse'; +import { ChannelAddress, Edge, EdgeConfig, Service, Utils, Websocket } from '../../../shared/shared'; +import { AbstractHistoryChart } from '../abstracthistorychart'; +import { ChartOptions, Data, DEFAULT_TIME_CHART_OPTIONS, TooltipItem } from './../shared'; +import { EnergyModalComponent } from './modal/modal.component'; type EnergyChartLabels = { production: string, diff --git a/ui/src/app/shared/jsonrpc/request/get24HoursPredictionRequest.ts b/ui/src/app/shared/jsonrpc/request/get24HoursPredictionRequest.ts new file mode 100644 index 00000000000..30ac302597c --- /dev/null +++ b/ui/src/app/shared/jsonrpc/request/get24HoursPredictionRequest.ts @@ -0,0 +1,34 @@ +import { ChannelAddress } from "../../../shared/type/channeladdress"; +import { format } from 'date-fns'; +import { JsonrpcRequest } from "../base"; +import { JsonRpcUtils } from "../jsonrpcutils"; + +/** + * Represents a JSON-RPC Request to query a 24 Hours Prediction. + * + *

    + * {
    + *   "jsonrpc": "2.0",
    + *   "id": UUID,
    + *   "method": "get24HoursPredictionRequest",
    + *   "params": {
    + *     "channels": ChannelAddress[]
    + *   }
    + * }
    + * 
    + */ +export class Get24HoursPredictionRequest extends JsonrpcRequest { + + static METHOD: string = "get24HoursPrediction"; + + public constructor( + private channels: ChannelAddress[] + ) { + super(Get24HoursPredictionRequest.METHOD, { + channels: JsonRpcUtils.channelsToStringArray(channels) + }); + // delete local fields, otherwise they are sent with the JSON-RPC Request + delete this.channels; + } + +} \ No newline at end of file diff --git a/ui/src/app/shared/jsonrpc/response/get24HoursPredictionResponse.ts b/ui/src/app/shared/jsonrpc/response/get24HoursPredictionResponse.ts new file mode 100644 index 00000000000..489e0512c21 --- /dev/null +++ b/ui/src/app/shared/jsonrpc/response/get24HoursPredictionResponse.ts @@ -0,0 +1,30 @@ +import { JsonrpcResponseSuccess } from "../base"; + +export class Prediction { + [channelAddress: string]: number[] +} + +/** + * Wraps a JSON-RPC Response for a Get24HoursPredictionRequest. + * + *
    + * {
    + *   "jsonrpc": "2.0",
    + *   "id": UUID,
    + *   "result": {
    + *     "componentId/channelId": [
    + *       value1, value2,...
    + *     ]
    + *   }
    + * }
    + * 
    + */ +export class Get24HoursPredictionResponse extends JsonrpcResponseSuccess { + + public constructor( + public readonly id: string, + public readonly result: Prediction + ) { + super(id, result); + } +} \ No newline at end of file