Browse Source

Merge branch 'lts-4.2' into lwm2m_fix_bug_null_point_profileleid_after_sleep_lts

pull/14403/head
nickAS21 10 months ago
parent
commit
5d7109aaf1
  1. 18
      application/pom.xml
  2. 79
      application/src/main/data/json/edge/instructions/install/centos/instructions.md
  3. 68
      application/src/main/data/json/edge/instructions/install/docker/instructions.md
  4. 60
      application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md
  5. 4
      application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md
  6. 7
      application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md
  7. 5
      application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md
  8. 5
      application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md
  9. 68
      application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md
  10. 2
      application/src/main/data/json/edge/instructions/upgrade/start_service.md
  11. 5
      application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md
  12. 6
      application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md
  13. 67
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java
  14. 37
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  15. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
  16. 18
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
  17. 7
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java
  18. 10
      application/src/main/java/org/thingsboard/server/service/edge/instructions/BaseEdgeInstallUpgradeInstructionsService.java
  19. 9
      application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java
  20. 24
      application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java
  21. 2
      application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java
  22. 2
      application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java
  23. 91
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  24. 27
      application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java
  25. 8
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java
  26. 21
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java
  27. 3
      application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmStatusSubCtx.java
  28. 8
      application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
  29. 75
      application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java
  30. 87
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  31. 24
      application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
  32. 52
      application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java
  33. 37
      application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
  34. 2
      common/data/pom.xml
  35. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java
  36. 2
      common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java
  37. 15
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  38. 4
      common/queue/pom.xml
  39. 5
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
  40. 2
      dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryCompositeKey.java
  41. 7
      dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java
  42. 68
      dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java
  43. 6
      dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java
  44. 111
      dao/src/test/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryDaoTest.java
  45. 2
      msa/js-executor/package.json
  46. 14
      msa/js-executor/yarn.lock
  47. 2
      msa/web-ui/package.json
  48. 4
      msa/web-ui/server.ts
  49. 14
      msa/web-ui/yarn.lock
  50. 32
      pom.xml
  51. 2
      rule-engine/rule-engine-api/pom.xml
  52. 6
      rule-engine/rule-engine-components/pom.xml
  53. 4
      tools/pom.xml
  54. 5
      ui-ngx/package.json
  55. 66
      ui-ngx/patches/@angular+common+18.2.13.patch
  56. 88
      ui-ngx/patches/@angular+compiler+18.2.13.patch
  57. 4
      ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts
  58. 16
      ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts
  59. 4
      ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts
  60. 2
      ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts
  61. 2
      ui-ngx/src/app/modules/home/pages/asset/asset.component.ts
  62. 2
      ui-ngx/src/app/modules/home/pages/device/device.component.ts
  63. 18
      ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html
  64. 2
      ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts
  65. 30
      ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts
  66. 2
      ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html
  67. 31
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts
  68. 2
      ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts
  69. 9
      ui-ngx/src/app/shared/models/ai-model.models.ts
  70. 4
      ui-ngx/src/app/shared/models/edge.models.ts
  71. 515
      ui-ngx/src/assets/locale/locale.constant-da_DK.json
  72. 365
      ui-ngx/src/assets/locale/locale.constant-de_DE.json
  73. 681
      ui-ngx/src/assets/locale/locale.constant-el_GR.json
  74. 8
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  75. 1449
      ui-ngx/src/assets/locale/locale.constant-fr_FR.json
  76. 1084
      ui-ngx/src/assets/locale/locale.constant-it_IT.json
  77. 1515
      ui-ngx/src/assets/locale/locale.constant-nl_NL.json
  78. 78
      ui-ngx/yarn.lock

18
application/pom.xml

@ -382,35 +382,35 @@
<artifactId>rocksdbjni</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-azure-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-google-ai-gemini</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-vertex-ai-gemini</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-mistral-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-bedrock</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-github-models</artifactId>
<exclusions>
<exclusion>
@ -420,7 +420,7 @@
</exclusions>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
</dependency>
</dependencies>

79
application/src/main/data/json/edge/instructions/install/centos/instructions.md

@ -1,7 +1,7 @@
Here is the list of commands, that can be used to quickly install ThingsBoard Edge on RHEL/CentOS 7/8 and connect to the server.
Here is the list of commands that can be used to quickly install ThingsBoard Edge on RHEL/CentOS 7/8 and connect to the server.
#### Prerequisites
Before continue to installation execute the following commands in order to install necessary tools:
Before continuing to installation, execute the following commands to install the necessary tools:
```bash
sudo yum install -y nano wget && sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
@ -9,29 +9,28 @@ sudo yum install -y nano wget && sudo yum install -y https://dl.fedoraproject.or
```
#### Step 1. Install Java 17 (OpenJDK)
ThingsBoard service is running on Java 17. Follow these instructions to install OpenJDK 17:
ThingsBoard service is running on Java 17. To install OpenJDK 17, follow these instructions:
```bash
sudo dnf install java-17-openjdk
{:copy-code}
```
Please don't forget to configure your operating system to use OpenJDK 17 by default.
You can configure which version is the default using the following command:
Configure your operating system to use OpenJDK 17 by default. You can configure the default version by running the following command:
```bash
sudo update-alternatives --config java
{:copy-code}
```
You can check the installation using the following command:
To check the installed Java version on your system, use the following command:
```bash
java -version
{:copy-code}
```
Expected command output is:
The expected result is:
```text
openjdk version "17.x.xx"
@ -39,14 +38,13 @@ OpenJDK Runtime Environment (...)
OpenJDK 64-Bit Server VM (build ...)
```
#### Step 2. Configure ThingsBoard Database
ThingsBoard Edge supports SQL and hybrid database approaches.
In this guide we will use SQL only.
For hybrid details please follow official installation instructions from the ThingsBoard documentation site.
#### Step 2. Configure ThingsBoard Edge Database
### PostgresSql
ThingsBoard Edge uses PostgreSQL database as a local storage.
To install PostgreSQL, follow the instructions below.
ThingsBoard Edge supports **SQL** and **hybrid** database configurations.
In this guide, we’ll use an **SQL** database.
For more details about the hybrid setup, please refer to the official installation instructions on the <a href="https://thingsboard.io/docs/user-guide/install/edge/rhel/#step-2-configure-thingsboard-database" target="_blank">ThingsBoard documentation site</a>.
To install the PostgreSQL database, run these commands:
```bash
# Update your system
@ -55,7 +53,8 @@ sudo dnf update
```
Install the repository RPM:
**For CentOS/RHEL 8:**
* **For CentOS/RHEL 8:**
```bash
# Install the repository RPM (For CentOS/RHEL 8):
@ -63,7 +62,7 @@ sudo sudo dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/
{:copy-code}
```
**For CentOS/RHEL 9:**
* **For CentOS/RHEL 9:**
```bash
# Install the repository RPM (for CentOS 9):
@ -111,22 +110,11 @@ sudo systemctl restart postgresql-16.service && psql -U postgres -d postgres -h
{:copy-code}
```
#### Step 3. Choose Queue Service
ThingsBoard Edge supports only Kafka or in-memory queue (since v4.0) for message storage and communication between ThingsBoard services.
How to choose the right queue implementation?
In Memory queue implementation is built-in and default. It is useful for development(PoC) environments and is not suitable for production deployments or any sort of cluster deployments.
Kafka is recommended for production deployments. This queue is used on the most of ThingsBoard production environments now.
In Memory queue is built in and enabled by default. No additional configuration is required.
#### Step 4. ThingsBoard Edge Service Installation
#### Step 3. ThingsBoard Edge Service Installation
Download installation package:
```bash
wget wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.rpm
wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.rpm
{:copy-code}
```
@ -137,8 +125,8 @@ sudo rpm -Uvh tb-edge-${TB_EDGE_TAG}.rpm
{:copy-code}
```
#### Step 5. Configure ThingsBoard Edge
To configure ThingsBoard Edge, you can use the following command to automatically update the configuration file with specific values:
#### Step 4. Configure ThingsBoard Edge
To configure ThingsBoard Edge, you can use the following command to automatically update the configuration file with specific values:
```bash
sudo sh -c 'cat <<EOL >> /etc/tb-edge/conf/tb-edge.conf
@ -151,7 +139,7 @@ EOL'
{:copy-code}
```
##### Configure PostgreSQL (Optional)
##### [Optional] Configure PostgreSQL Connection
If you changed PostgreSQL default datasource settings, use the following command:
```bash
@ -163,11 +151,10 @@ EOL'
{:copy-code}
```
PUT_YOUR_POSTGRESQL_PASSWORD_HERE: Replace with your actual PostgreSQL user password.
* **PUT_YOUR_POSTGRESQL_PASSWORD_HERE**: Replace with your actual **PostgreSQL user password**.
##### [Optional] Update bind ports
If ThingsBoard Edge is going to be running on the same machine where ThingsBoard server (cloud) is running, you'll need to update configuration parameters to avoid port collision between ThingsBoard server and ThingsBoard Edge.
##### [Optional] Update Bind Ports
If ThingsBoard Edge runs on the same machine as the ThingsBoard Server, you need to update the port configuration to avoid conflicts between the two services.
Please execute the following command to update ThingsBoard Edge configuration file (**/etc/tb-edge/conf/tb-edge.conf**):
@ -182,28 +169,26 @@ EOL'
{:copy-code}
```
Make sure that ports above (18080, 11883, 15683) are not used by any other application.
Make sure that ports **18080**, **11883**, and **15683–15688** are not being used by any other applications.
#### Step 5. Run Installation Script
#### Step 6. Run installation Script
Once ThingsBoard Edge is installed and configured please execute the following install script:
Once ThingsBoard Edge is installed and configured, please execute the following install script:
```bash
sudo /usr/share/tb-edge/bin/install/install.sh
{:copy-code}
```
#### Step 7. Restart ThingsBoard Edge Service
#### Step 6. Start ThingsBoard Edge Service
```bash
sudo service tb-edge restart
sudo service tb-edge start
{:copy-code}
```
#### Step 8. Open ThingsBoard Edge UI
Once started, you will be able to open **ThingsBoard Edge UI** using the following link http://localhost:8080.
###### NOTE: Edge HTTP bind port update
#### Step 7. Open ThingsBoard Edge UI
If the Edge HTTP bind port was changed to 18080 during Edge installation, access the ThingsBoard Edge instance at http://localhost:18080.
Once the Edge service has started, open the Edge web interface at http://localhost:8080, or http://localhost:18080 if you modified the HTTP bind port configuration in the previous step.
Log in using your **tenant credentials** from either your local ThingsBoard Server or the **ThingsBoard Live Demo**.

68
application/src/main/data/json/edge/instructions/install/docker/instructions.md

@ -1,34 +1,17 @@
Here is the list of commands, that can be used to quickly install ThingsBoard Edge using docker compose and connect to the server.
Here is the list of commands that can be used to quickly install ThingsBoard Edge using docker compose and connect to the server.
#### Prerequisites
Install <a href="https://docs.docker.com/engine/install/" target="_blank"> Docker CE</a> and <a href="https://docs.docker.com/compose/install/" target="_blank"> Docker Compose</a>.
#### Step 1. Running ThingsBoard Edge
#### Step 1. Create the ThingsBoard Edge Docker Compose file
Here you can find ThingsBoard Edge docker image:
ThingsBoard Edge supports both **in-memory** and **Kafka** queues for message storage and communication between ThingsBoard services.
It also supports **SQL** and **hybrid** database configurations.
In this guide, we’ll use the **in-memory** queue and an **SQL** database.
For more details about the hybrid setup, please refer to the official installation instructions on the <a href="https://thingsboard.io/docs/user-guide/install/edge/docker/#step-2-choose-queue-andor-database-services" target="_blank">ThingsBoard documentation site</a>.
<a href="https://hub.docker.com/r/thingsboard/tb-edge" target="_blank"> thingsboard/tb-edge</a>
#### Step 2. Choose Queue and/or Database Services
ThingsBoard Edge supports only Kafka or in-memory queue (since v4.0) for message storage and communication between ThingsBoard services.
ThingsBoard Edge supports SQL and hybrid database approaches.
In this guide we will use SQL only.
For hybrid details please follow official installation instructions from the ThingsBoard documentation site.
How to choose the right queue implementation?
In Memory queue implementation is built-in and default. It is useful for development(PoC) environments and is not suitable for production deployments or any sort of cluster deployments.
Kafka is recommended for production deployments. This queue is used on the most of ThingsBoard production environments now.
Hybrid implementation combines PostgreSQL and Cassandra databases with Kafka queue service. It is recommended if you plan to manage 1M+ devices in production or handle high data ingestion rate (more than 5000 msg/sec).
Create a docker compose file for the ThingsBoard Edge service:
##### In Memory
Now, create a Docker Compose file for the ThingsBoard Edge service:
```bash
nano docker-compose.yml
@ -38,7 +21,6 @@ nano docker-compose.yml
Add the following lines to the yml file:
```bash
version: '3.8'
services:
mytbedge:
restart: always
@ -54,6 +36,7 @@ services:
CLOUD_RPC_HOST: ${BASE_URL}
CLOUD_RPC_PORT: ${CLOUD_RPC_PORT}
CLOUD_RPC_SSL_ENABLED: ${CLOUD_RPC_SSL_ENABLED}
${EXTRA_HOSTS}
volumes:
- tb-edge-data:/data
- tb-edge-logs:/var/log/tb-edge
@ -78,41 +61,28 @@ volumes:
{:copy-code}
```
##### [Optional] Update bind ports
If ThingsBoard Edge is set to run on the same machine where the ThingsBoard server is operating, you need to update port configuration to prevent port collision between the ThingsBoard server and ThingsBoard Edge.
##### [Optional] Update Bind Ports
If ThingsBoard Edge runs on the same machine as the ThingsBoard Server, you need to update the port configuration to avoid conflicts between the two services.
Ensure that the ports 18080, 11883, 15683-15688 are not used by any other application.
Make sure that ports **18080**, **11883**, and **15683–15688** are not being used by any other applications.
Then, update the port configuration in the docker-compose.yml file:
```bash
sed -i ‘s/8080:8080/18080:8080/; s/1883:1883/11883:1883/; s/5683-5688:5683-5688\/udp/15683-15688:5683-5688\/udp/’ docker-compose.yml
{:copy-code}
```
#### Start ThingsBoard Edge
Set the terminal in the directory which contains the docker-compose.yml file and execute the following commands to up this docker compose directly:
Then, update the port configuration in the `docker-compose.yml` file accordingly:
```bash
docker compose up -d && docker compose logs -f mytbedge
sed -i 's/8080:8080/18080:8080/; s/1883:1883/11883:1883/; s/5683-5688:5683-5688\/udp/15683-15688:5683-5688\/udp/' docker-compose.yml
{:copy-code}
```
###### NOTE: Docker Compose V2 vs docker-compose (with a hyphen)
ThingsBoard supports Docker Compose V2 (Docker Desktop or Compose plugin) starting from **3.4.2** release, because **docker-compose** as standalone setup is no longer supported by Docker.
We **strongly** recommend to update to Docker Compose V2 and use it.
If you still rely on using Docker Compose as docker-compose (with a hyphen), then please execute the following commands to start ThingsBoard Edge:
#### Step 2. Start ThingsBoard Edge
Navigate to the directory containing the `docker-compose.yml` file and run the following command to start the ThingsBoard Edge service:
```bash
docker-compose up -d
docker-compose logs -f mytbedge
docker compose up -d && docker compose logs -f mytbedge
{:copy-code}
```
#### Step 3. Open ThingsBoard Edge UI
Once the Edge service is started, open the Edge UI at http://localhost:8080.
###### NOTE: Edge HTTP bind port update
If the Edge HTTP bind port was changed to 18080 during Edge installation, access the ThingsBoard Edge instance at http://localhost:18080.
Once the Edge service has started, open the Edge web interface at http://localhost:8080, or http://localhost:18080 if you modified the HTTP bind port configuration in the previous step.
Please use your tenant credentials from local Server instance or ThingsBoard Live Demo to log in to the ThingsBoard Edge.
Log in using your **tenant credentials** from either your local ThingsBoard Server or the **ThingsBoard Live Demo**.

60
application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md

@ -1,4 +1,4 @@
Here is the list of commands, that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the server.
Here is the list of commands that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the server.
#### Step 1. Install Java 17 (OpenJDK)
ThingsBoard service is running on Java 17. To install OpenJDK 17, follow these instructions:
@ -25,19 +25,16 @@ java -version
The expected result is:
```text
openjdk version "17.x.xx"
openjdk version "17.x.xx"
OpenJDK Runtime Environment (...)
OpenJDK 64-Bit Server VM (...)
OpenJDK 64-Bit Server VM (build ...)
```
#### Step 2. Configure ThingsBoard Edge Database
ThingsBoard Edge supports SQL and hybrid database approaches.
In this guide we will use SQL only.
For hybrid details please follow official installation instructions from the ThingsBoard documentation site.
### Configure PostgreSQL
ThingsBoard Edge uses PostgreSQL database as a local storage.
ThingsBoard Edge supports **SQL** and **hybrid** database configurations.
In this guide, we’ll use an **SQL** database.
For more details about the hybrid setup, please refer to the official installation instructions on the <a href="https://thingsboard.io/docs/user-guide/install/edge/deb-installation/#step-2-configure-the-thingsboard-edge-database" target="_blank">ThingsBoard documentation site</a>.
To install the PostgreSQL database, run these commands:
@ -71,18 +68,8 @@ echo "CREATE DATABASE tb_edge;" | psql -U postgres -d postgres -h 127.0.0.1 -W
{:copy-code}
```
#### Step 3. Choose Queue Service
ThingsBoard Edge supports only Kafka or in-memory queue (since v4.0) for message storage and communication between ThingsBoard services. Choose the appropriate queue implementation based on your specific business needs:
In Memory: The built-in and default queue implementation. It is useful for development or proof-of-concept (PoC) environments, but is not recommended for production or any type of clustered deployments due to limited scalability.
Kafka: Recommended for production deployments. This queue is used in the most of ThingsBoard production environments now.
In Memory queue is built in and enabled by default. No additional configuration is required.
#### Step 4. ThingsBoard Edge Service Installation
Download installation package:
#### Step 3. ThingsBoard Edge Service Installation
Download the installation package:
```bash
wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.deb
@ -96,8 +83,8 @@ sudo dpkg -i tb-edge-${TB_EDGE_TAG}.deb
{:copy-code}
```
#### Step 5. Configure ThingsBoard Edge
To configure ThingsBoard Edge, you can use the following command to automatically update the configuration file with specific values:
#### Step 4. Configure ThingsBoard Edge
To configure ThingsBoard Edge, you can use the following command to automatically update the configuration file with specific values:
```bash
sudo sh -c 'cat <<EOL >> /etc/tb-edge/conf/tb-edge.conf
@ -110,7 +97,7 @@ EOL'
{:copy-code}
```
##### [Optional] Configure PostgreSQL
##### [Optional] Configure PostgreSQL Connection
If you changed PostgreSQL default datasource settings, use the following command:
```bash
@ -122,10 +109,10 @@ EOL'
{:copy-code}
```
PUT_YOUR_POSTGRESQL_PASSWORD_HERE: Replace with your actual PostgreSQL user password.
* **PUT_YOUR_POSTGRESQL_PASSWORD_HERE**: Replace with your actual **PostgreSQL user password**.
##### [Optional] Update bind ports
If ThingsBoard Edge is going to be running on the same machine where ThingsBoard server (cloud) is running, you'll need to update configuration parameters to avoid port collision between ThingsBoard server and ThingsBoard Edge.
##### [Optional] Update Bind Ports
If ThingsBoard Edge runs on the same machine as the ThingsBoard Server, you need to update the port configuration to avoid conflicts between the two services.
Please execute the following command to update ThingsBoard Edge configuration file (**/etc/tb-edge/conf/tb-edge.conf**):
@ -140,29 +127,26 @@ EOL'
{:copy-code}
```
Make sure that ports above (18080, 11883, 15683) are not used by any other application.
Make sure that ports **18080**, **11883**, and **15683–15688** are not being used by any other applications.
#### Step 6. Run installation Script
#### Step 5. Run Installation Script
Once ThingsBoard Edge is installed and configured please execute the following install script:
Once ThingsBoard Edge is installed and configured, please execute the following installation script:
```bash
sudo /usr/share/tb-edge/bin/install/install.sh
{:copy-code}
```
#### Step 7. Restart ThingsBoard Edge Service
#### Step 6. Start ThingsBoard Edge Service
```bash
sudo service tb-edge restart
sudo service tb-edge start
{:copy-code}
```
#### Step 8. Open ThingsBoard Edge UI
Once started, you will be able to open **ThingsBoard Edge UI** using the following link http://localhost:8080.
###### NOTE: Edge HTTP bind port update
#### Step 7. Open ThingsBoard Edge UI
Use next **ThingsBoard Edge UI** link **http://localhost:18080** if you updated HTTP 8080 bind port to **18080**.
Once the Edge service has started, open the Edge web interface at http://localhost:8080, or http://localhost:18080 if you modified the HTTP bind port configuration in the previous step.
Log in using your **tenant credentials** from either your local ThingsBoard Server or the **ThingsBoard Live Demo**.

4
application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md

@ -1,15 +1,15 @@
#### Upgrading to ${TB_EDGE_VERSION}EDGE
**ThingsBoard Edge package download:**
ThingsBoard Edge package download:
```bash
wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.rpm
{:copy-code}
```
##### ThingsBoard Edge service upgrade
Install package:
```bash
sudo rpm -Uvh tb-edge-${TB_EDGE_TAG}.rpm
{:copy-code}
```
${UPGRADE_DB}

7
application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md

@ -1,10 +1,3 @@
#### Upgrading to ${TB_EDGE_VERSION}
Execute the following command to pull **${TB_EDGE_VERSION}** image:
```bash
docker pull thingsboard/tb-edge:${TB_EDGE_VERSION}
{:copy-code}
```
${UPGRADE_DB}

5
application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md

@ -1,11 +1,10 @@
Modify ‘main’ docker compose (`docker-compose.yml`) file for ThingsBoard Edge and update version of the image:
Modify ‘main’ docker compose (`docker-compose.yml`) a file for ThingsBoard Edge and update a version of the image:
```bash
nano docker-compose.yml
{:copy-code}
```
```text
version: '3.8'
services:
mytbedge:
restart: always
@ -13,7 +12,7 @@ services:
...
```
Make sure your image is the set to **tb-edge-${TB_EDGE_VERSION}**.
Make sure your image is set to **tb-edge:${TB_EDGE_VERSION}**.
Execute the following commands to up this docker compose directly:
```bash

5
application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md

@ -1,4 +1,4 @@
Create docker compose file for ThingsBoard Edge upgrade process:
Create a docker compose file for ThingsBoard Edge upgrade process:
```bash
> docker-compose-upgrade.yml && nano docker-compose-upgrade.yml
@ -8,7 +8,6 @@ Create docker compose file for ThingsBoard Edge upgrade process:
Add the following lines to the yml file:
```bash
version: '3.8'
services:
mytbedge:
restart: on-failure
@ -40,7 +39,7 @@ volumes:
{:copy-code}
```
Execute the following command to start upgrade process:
Execute the following command to start an upgrade process:
```bash
docker compose -f docker-compose-upgrade.yml up --abort-on-container-exit

68
application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md

@ -1,6 +1,6 @@
Here is the list of commands, that can be used to quickly upgrade ThingsBoard Edge on Docker (Linux or MacOS).
Here is the list of commands that can be used to quickly upgrade ThingsBoard Edge on Docker (Linux or macOS).
#### Prepare for upgrading ThingsBoard Edge
#### Prepare for Upgrading ThingsBoard Edge
Set the terminal in the directory which contains the `docker-compose.yml` file and execute the following command
to stop and remove currently running TB Edge container:
@ -10,70 +10,6 @@ docker compose rm mytbedge
{:copy-code}
```
**OPTIONAL:** If you still rely on Docker Compose as docker-compose (with a hyphen) here is the list of the above commands:
```text
docker-compose stop
docker-compose rm mytbedge
```
##### Migrating Data from Docker Bind Mount Folders to Docker Volumes
Starting with the **3.6.2** release, the ThingsBoard team has transitioned from using Docker bind mount folders to Docker volumes.
This change aims to enhance security and efficiency in storing data for Docker containers and to mitigate permission issues across various environments.
To migrate from Docker bind mounts to Docker volumes, please execute the following commands:
```bash
docker run --rm -v tb-edge-data:/volume -v ~/.mytb-edge-data:/backup busybox sh -c "cp -a /backup/. /volume"
docker run --rm -v tb-edge-logs:/volume -v ~/.mytb-edge-logs:/backup busybox sh -c "cp -a /backup/. /volume"
docker run --rm -v tb-edge-postgres-data:/volume -v ~/.mytb-edge-data/db:/backup busybox sh -c "cp -a /backup/. /volume"
{:copy-code}
```
After completing the data migration to the newly created Docker volumes, you'll need to update the volume mounts in your Docker Compose configuration.
Modify the `docker-compose.yml` file for ThingsBoard Edge to update the volume settings.
Update volume mounts. Locate the following snippet:
```text
volumes:
- ~/.mytb-edge-data:/data
- ~/.mytb-edge-logs:/var/log/tb-edge
...
```
And replace it with:
```text
volumes:
- tb-edge-data:/data
- tb-edge-logs:/var/log/tb-edge
...
```
Apply a similar update for the PostgreSQL service. Find the section:
```text
volumes:
- ~/.mytb-edge-data/db:/var/lib/postgresql/data
...
```
And replace it with:
```text
volumes:
- tb-edge-postgres-data:/var/lib/postgresql/data
...
```
Finally, please add next volumes section at the end of the file:
```text
...
volumes:
tb-edge-data:
name: tb-edge-data
tb-edge-logs:
name: tb-edge-logs
tb-edge-postgres-data:
name: tb-edge-postgres-data
```
##### Backup Database
Make a copy of the database volume before upgrading:

2
application/src/main/data/json/edge/instructions/upgrade/start_service.md

@ -1,4 +1,4 @@
Start the service
#### Start the Service
```bash
sudo systemctl tb-edge start

5
application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md

@ -1,15 +1,14 @@
#### Upgrading to ${TB_EDGE_VERSION}EDGE
**ThingsBoard Edge package download:**
ThingsBoard Edge package download:
```bash
wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.deb
{:copy-code}
```
##### ThingsBoard Edge service upgrade
Install package:
```bash
sudo dpkg -i tb-edge-${TB_EDGE_TAG}.deb
{:copy-code}
```
${UPGRADE_DB}

6
application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md

@ -1,6 +1,6 @@
Here is the list of commands, that can be used to quickly upgrade ThingsBoard Edge on ${OS}
Here is the list of commands that can be used to quickly upgrade ThingsBoard Edge on ${OS}
#### Prepare for upgrading ThingsBoard Edge
#### Prepare for Upgrading ThingsBoard Edge
Stop ThingsBoard Edge service:
@ -33,4 +33,4 @@ sudo -Hiu postgres pg_dump tb_edge > tb_edge.sql.bak
{:copy-code}
```
Check backup file created successfully.
Check the backup file created successfully.

67
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java

@ -23,7 +23,6 @@ import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg;
@ -31,21 +30,14 @@ import org.thingsboard.server.actors.calculatedField.MultipleTbCallback;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
@ -70,9 +62,6 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry;
import java.util.ArrayList;
@ -80,12 +69,15 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.DataConstants.SCOPE;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultAttributeEntry;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultTsKvEntry;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformSingleValueArgument;
import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto;
@TbRuleEngineComponent
@ -244,30 +236,17 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
private ListenableFuture<ArgumentEntry> fetchKvEntry(TenantId tenantId, EntityId entityId, Argument argument) {
return switch (argument.getRefEntityKey().getType()) {
case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument);
case ATTRIBUTE -> transformSingleValueArgument(
Futures.transform(
attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()),
result -> result.or(() -> Optional.of(new BaseAttributeKvEntry(createDefaultKvEntry(argument), System.currentTimeMillis(), 0L))),
calculatedFieldCallbackExecutor)
);
case TS_LATEST -> transformSingleValueArgument(
Futures.transform(
timeseriesService.findLatest(tenantId, entityId, argument.getRefEntityKey().getKey()),
result -> result.or(() -> Optional.of(new BasicTsKvEntry(System.currentTimeMillis(), createDefaultKvEntry(argument), 0L))),
calculatedFieldCallbackExecutor));
case ATTRIBUTE -> Futures.transform(
attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()),
result -> transformSingleValueArgument(result.orElseGet(() -> createDefaultAttributeEntry(argument, System.currentTimeMillis()))),
calculatedFieldCallbackExecutor);
case TS_LATEST -> Futures.transform(
timeseriesService.findLatest(tenantId, entityId, argument.getRefEntityKey().getKey()),
result -> transformSingleValueArgument(result.orElseGet(() -> createDefaultTsKvEntry(argument, System.currentTimeMillis()))),
calculatedFieldCallbackExecutor);
};
}
private ListenableFuture<ArgumentEntry> transformSingleValueArgument(ListenableFuture<Optional<? extends KvEntry>> kvEntryFuture) {
return Futures.transform(kvEntryFuture, kvEntry -> {
if (kvEntry.isPresent() && kvEntry.get().getValue() != null) {
return ArgumentEntry.createSingleValueArgument(kvEntry.get());
} else {
return new SingleValueArgumentEntry();
}
}, calculatedFieldCallbackExecutor);
}
private ListenableFuture<ArgumentEntry> fetchTsRolling(TenantId tenantId, EntityId entityId, Argument argument) {
long currentTime = System.currentTimeMillis();
long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow();
@ -282,28 +261,6 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
return Futures.transform(tsRollingFuture, tsRolling -> tsRolling == null ? new TsRollingArgumentEntry(limit, timeWindow) : ArgumentEntry.createTsRollingArgument(tsRolling, limit, timeWindow), calculatedFieldCallbackExecutor);
}
private KvEntry createDefaultKvEntry(Argument argument) {
String key = argument.getRefEntityKey().getKey();
String defaultValue = argument.getDefaultValue();
if (StringUtils.isBlank(defaultValue)) {
return new StringDataEntry(key, null);
}
if (NumberUtils.isParsable(defaultValue)) {
return new DoubleDataEntry(key, Double.parseDouble(defaultValue));
}
if ("true".equalsIgnoreCase(defaultValue) || "false".equalsIgnoreCase(defaultValue)) {
return new BooleanDataEntry(key, Boolean.parseBoolean(defaultValue));
}
return new StringDataEntry(key, defaultValue);
}
private CalculatedFieldState createStateByType(CalculatedFieldCtx ctx) {
return switch (ctx.getCfType()) {
case SIMPLE -> new SimpleCalculatedFieldState(ctx.getArgNames());
case SCRIPT -> new ScriptCalculatedFieldState(ctx.getArgNames());
};
}
private static class TbCallbackWrapper implements TbQueueCallback {
private final TbCallback callback;

37
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java

@ -31,19 +31,19 @@ import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArg
@AllArgsConstructor
public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
static final long DEFAULT_LAST_UPDATE_TS = -1L;
protected List<String> requiredArguments;
protected Map<String, ArgumentEntry> arguments;
protected boolean sizeExceedsLimit;
protected long latestTimestamp = -1;
public BaseCalculatedFieldState(List<String> requiredArguments) {
this.requiredArguments = requiredArguments;
this.arguments = new HashMap<>();
}
public BaseCalculatedFieldState() {
this(new ArrayList<>(), new HashMap<>(), false, -1);
this(new ArrayList<>(), new HashMap<>(), false);
}
@Override
@ -73,7 +73,6 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
if (entryUpdated) {
stateUpdated = true;
updateLastUpdateTimestamp(newEntry);
}
}
@ -109,15 +108,29 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
protected abstract void validateNewEntry(ArgumentEntry newEntry);
private void updateLastUpdateTimestamp(ArgumentEntry entry) {
long newTs = this.latestTimestamp;
if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) {
newTs = singleValueArgumentEntry.getTs();
} else if (entry instanceof TsRollingArgumentEntry tsRollingArgumentEntry) {
Map.Entry<Long, Double> lastEntry = tsRollingArgumentEntry.getTsRecords().lastEntry();
newTs = (lastEntry != null) ? lastEntry.getKey() : System.currentTimeMillis();
public long getLatestTimestamp() {
long latestTs = DEFAULT_LAST_UPDATE_TS;
boolean allDefault = arguments.values().stream().allMatch(entry -> {
if (entry instanceof SingleValueArgumentEntry single) {
return single.isDefaultValue();
}
return false;
});
for (ArgumentEntry entry : arguments.values()) {
if (entry instanceof SingleValueArgumentEntry single) {
if (allDefault) {
latestTs = Math.max(latestTs, single.getTs());
} else if (!single.isDefaultValue()) {
latestTs = Math.max(latestTs, single.getTs());
}
} else if (entry instanceof TsRollingArgumentEntry rolling) {
latestTs = Math.max(latestTs, rolling.getLatestTs());
}
}
this.latestTimestamp = Math.max(this.latestTimestamp, newTs);
return latestTs;
}
}

2
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java

@ -100,7 +100,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
}
long latestTs = getLatestTimestamp();
if (useLatestTs && latestTs != -1) {
if (useLatestTs && latestTs != DEFAULT_LAST_UPDATE_TS) {
ObjectNode resultNode = JacksonUtil.newObjectNode();
resultNode.put("ts", latestTs);
resultNode.set("values", valuesNode);

18
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java

@ -19,7 +19,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
@ -32,17 +31,25 @@ import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SingleValueArgumentEntry implements ArgumentEntry {
public static final Long DEFAULT_VERSION = -1L;
private long ts;
private BasicKvEntry kvEntryValue;
private Long version;
private boolean forceResetPrevious;
public SingleValueArgumentEntry() {
this.ts = DEFAULT_LAST_UPDATE_TS;
this.version = DEFAULT_VERSION;
}
public SingleValueArgumentEntry(TsKvProto entry) {
this.ts = entry.getTs();
if (entry.hasVersion()) {
@ -112,7 +119,7 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
@Override
public boolean updateEntry(ArgumentEntry entry) {
if (entry instanceof SingleValueArgumentEntry singleValueEntry) {
if (singleValueEntry.getTs() <= this.ts) {
if (singleValueEntry.getTs() < this.ts) {
return false;
}
@ -128,4 +135,9 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
}
return false;
}
public boolean isDefaultValue() {
return DEFAULT_VERSION.equals(this.version);
}
}

7
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java

@ -31,6 +31,8 @@ import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ -83,6 +85,11 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
return tsRecords;
}
public long getLatestTs() {
var lastEntry = tsRecords.lastEntry();
return (lastEntry != null) ? lastEntry.getKey() : DEFAULT_LAST_UPDATE_TS;
}
@Override
public TbelCfArg toTbelCfArg() {
List<TbelCfTsDoubleVal> values = new ArrayList<>(tsRecords.size());

10
application/src/main/java/org/thingsboard/server/service/edge/instructions/BaseEdgeInstallUpgradeInstructionsService.java

@ -18,7 +18,6 @@ package org.thingsboard.server.service.edge.instructions;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.server.service.install.InstallScripts;
import java.io.IOException;
@ -26,6 +25,8 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.thingsboard.edge.rpc.EdgeGrpcClient.getNewestEdgeVersion;
@Slf4j
@RequiredArgsConstructor
public abstract class BaseEdgeInstallUpgradeInstructionsService {
@ -35,9 +36,12 @@ public abstract class BaseEdgeInstallUpgradeInstructionsService {
private final InstallScripts installScripts;
@Value("${app.version:unknown}")
@Setter
protected String appVersion;
protected String platformEdgeVersion = convertEdgeVersionToDocsFormat(getNewestEdgeVersion().name());
protected String convertEdgeVersionToDocsFormat(String edgeVersion) {
return edgeVersion.replace("_", ".").substring(2);
}
protected String readFile(Path file) {
try {

9
application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java

@ -65,9 +65,7 @@ public class DefaultEdgeInstallInstructionsService extends BaseEdgeInstallUpgrad
dockerInstallInstructions = dockerInstallInstructions.replace("${EXTRA_HOSTS}", "");
dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", baseUrl);
}
String edgeVersion = appVersion + "EDGE";
edgeVersion = edgeVersion.replace("-SNAPSHOT", "");
dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion);
dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", platformEdgeVersion + "EDGE");
dockerInstallInstructions = replacePlaceholders(dockerInstallInstructions, edge);
return new EdgeInstructions(dockerInstallInstructions);
}
@ -76,9 +74,8 @@ public class DefaultEdgeInstallInstructionsService extends BaseEdgeInstallUpgrad
String ubuntuInstallInstructions = readFile(resolveFile(os, "instructions.md"));
ubuntuInstallInstructions = replacePlaceholders(ubuntuInstallInstructions, edge);
ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${BASE_URL}", request.getServerName());
String edgeVersion = appVersion.replace("-SNAPSHOT", "");
ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion);
ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_TAG}", getTagVersion(edgeVersion));
ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_VERSION}", platformEdgeVersion);
ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_TAG}", getTagVersion(platformEdgeVersion));
return new EdgeInstructions(ubuntuInstallInstructions);
}

24
application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java

@ -52,12 +52,11 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad
@Override
public EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod) {
String tbVersion = appVersion.replace("-SNAPSHOT", "");
String currentEdgeVersion = convertEdgeVersionToDocsFormat(edgeVersion);
return switch (upgradeMethod.toLowerCase()) {
case "docker" -> getDockerUpgradeInstructions(tbVersion, currentEdgeVersion);
case "docker" -> getDockerUpgradeInstructions(this.platformEdgeVersion, currentEdgeVersion);
case "ubuntu", "centos" ->
getLinuxUpgradeInstructions(tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase());
getLinuxUpgradeInstructions(this.platformEdgeVersion, currentEdgeVersion, upgradeMethod.toLowerCase());
default -> throw new IllegalArgumentException("Unsupported upgrade method for Edge: " + upgradeMethod);
};
}
@ -74,8 +73,7 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad
Optional<AttributeKvEntry> attributeKvEntryOpt = attributesService.find(tenantId, edgeId, AttributeScope.SERVER_SCOPE, DataConstants.EDGE_VERSION_ATTR_KEY).get();
if (attributeKvEntryOpt.isPresent()) {
String edgeVersionFormatted = convertEdgeVersionToDocsFormat(attributeKvEntryOpt.get().getValueAsString());
String appVersionFormatted = appVersion.replace("-SNAPSHOT", "");
return isVersionGreaterOrEqualsThan(edgeVersionFormatted, "3.6.0") && !isVersionGreaterOrEqualsThan(edgeVersionFormatted, appVersionFormatted);
return isVersionGreaterOrEqualsThan(edgeVersionFormatted, "3.6.0") && !isVersionGreaterOrEqualsThan(edgeVersionFormatted, platformEdgeVersion);
}
return false;
}
@ -98,13 +96,13 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad
return true;
}
private EdgeInstructions getDockerUpgradeInstructions(String tbVersion, String currentEdgeVersion) {
private EdgeInstructions getDockerUpgradeInstructions(String platformEdgeVersion, String currentEdgeVersion) {
EdgeUpgradeInfo edgeUpgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion);
if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) {
if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || platformEdgeVersion.equals(currentEdgeVersion)) {
return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available.");
}
StringBuilder result = new StringBuilder(readFile(resolveFile("docker", "upgrade_preparing.md")));
while (edgeUpgradeInfo.getNextEdgeVersion() != null && !tbVersion.equals(currentEdgeVersion)) {
while (edgeUpgradeInfo.getNextEdgeVersion() != null && !platformEdgeVersion.equals(currentEdgeVersion)) {
String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion();
String dockerUpgradeInstructions = readFile(resolveFile("docker", "instructions.md"));
if (edgeUpgradeInfo.isRequiresUpdateDb()) {
@ -125,15 +123,15 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad
return new EdgeInstructions(result.toString());
}
private EdgeInstructions getLinuxUpgradeInstructions(String tbVersion, String currentEdgeVersion, String os) {
private EdgeInstructions getLinuxUpgradeInstructions(String platformEdgeVersion, String currentEdgeVersion, String os) {
EdgeUpgradeInfo edgeUpgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion);
if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) {
if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || platformEdgeVersion.equals(currentEdgeVersion)) {
return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available.");
}
String upgrade_preparing = readFile(resolveFile("upgrade_preparing.md"));
upgrade_preparing = upgrade_preparing.replace("${OS}", os.equals("centos") ? "RHEL/CentOS 7/8" : "Ubuntu");
StringBuilder result = new StringBuilder(upgrade_preparing);
while (edgeUpgradeInfo.getNextEdgeVersion() != null && !tbVersion.equals(currentEdgeVersion)) {
while (edgeUpgradeInfo.getNextEdgeVersion() != null && !platformEdgeVersion.equals(currentEdgeVersion)) {
String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion();
String linuxUpgradeInstructions = readFile(resolveFile(os, "instructions.md"));
if (edgeUpgradeInfo.isRequiresUpdateDb()) {
@ -155,10 +153,6 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad
return new EdgeInstructions(result.toString());
}
private String convertEdgeVersionToDocsFormat(String edgeVersion) {
return edgeVersion.replace("_", ".").substring(2);
}
@Override
protected String getBaseDirName() {
return UPGRADE_DIR;

2
application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java

@ -23,6 +23,6 @@ public interface EdgeInstallInstructionsService {
EdgeInstructions getInstallInstructions(Edge edge, String installationMethod, HttpServletRequest request);
void setAppVersion(String version);
void setPlatformEdgeVersion(String version);
}

2
application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java

@ -28,7 +28,7 @@ public interface EdgeUpgradeInstructionsService {
void updateInstructionMap(Map<String, EdgeUpgradeInfo> upgradeVersions);
void setAppVersion(String version);
void setPlatformEdgeVersion(String version);
boolean isUpgradeAvailable(TenantId tenantId, EdgeId edgeId) throws Exception;

91
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -69,6 +69,7 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -82,6 +83,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.ACTIVITY_STATE;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_CONNECT_TIME;
@ -94,6 +96,7 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS
public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService {
private final ConcurrentMap<EdgeId, EdgeGrpcSession> sessions = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, EdgeGrpcSession> sessionsById = new ConcurrentHashMap<>();
private final ConcurrentMap<EdgeId, Lock> sessionNewEventsLocks = new ConcurrentHashMap<>();
private final Map<EdgeId, Boolean> sessionNewEvents = new HashMap<>();
private final ConcurrentMap<EdgeId, ScheduledFuture<?>> sessionEdgeEventChecks = new ConcurrentHashMap<>();
@ -283,6 +286,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
destroySession(session);
session.cleanUp();
sessions.remove(edgeId);
sessionsById.remove(session.getSessionId());
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
@ -332,9 +336,15 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
TenantId tenantId = edge.getTenantId();
log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId);
if (sessions.containsKey(edgeId)) {
destroySession(sessions.get(edgeId));
EdgeGrpcSession existing = sessions.get(edgeId);
if (existing != null) {
log.info("[{}][{}] Replacing existing session [{}] for edge [{}]", tenantId, edgeGrpcSession.getSessionId(), existing.getSessionId(), edgeId);
destroySession(existing);
sessionsById.remove(existing.getSessionId());
}
}
sessions.put(edgeId, edgeGrpcSession);
sessionsById.put(edgeGrpcSession.getSessionId(), edgeGrpcSession);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
@ -492,9 +502,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void onEdgeDisconnect(Edge edge, UUID sessionId) {
EdgeId edgeId = edge.getId();
log.info("[{}][{}] edge disconnected!", edgeId, sessionId);
EdgeGrpcSession toRemove = sessions.get(edgeId);
if (toRemove.getSessionId().equals(sessionId)) {
toRemove = sessions.remove(edgeId);
EdgeGrpcSession current = sessions.get(edgeId);
if (current != null && current.getSessionId().equals(sessionId)) {
EdgeGrpcSession toRemove = sessions.remove(edgeId);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
@ -503,6 +513,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
newEventLock.unlock();
}
destroySession(toRemove);
sessionsById.remove(sessionId);
TenantId tenantId = toRemove.getEdge().getTenantId();
save(tenantId, edgeId, ACTIVITY_STATE, false);
long lastDisconnectTs = System.currentTimeMillis();
@ -510,7 +521,18 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edge, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT);
cancelScheduleEdgeEventsCheck(edgeId);
} else {
log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId);
log.info("[{}] edge session [{}] is not current anymore. Attempting to destroy it by sessionId.", edgeId, sessionId);
EdgeGrpcSession stale = sessionsById.remove(sessionId);
if (stale != null) {
try {
destroySession(stale);
log.info("[{}][{}] Successfully destroyed stale session for edge [{}]", stale.getTenantId(), sessionId, edgeId);
} catch (Exception e) {
log.warn("[{}][{}] Failed to destroy stale session for edge [{}]", stale.getTenantId(), sessionId, edgeId, e);
}
} else {
log.debug("[{}] No session found by sessionId [{}] to destroy", edgeId, sessionId);
}
}
edgeIdServiceIdCache.evict(edgeId);
}
@ -522,6 +544,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId());
zombieSessions.add(session);
}
} catch (Exception e) {
log.warn("[{}][{}] Exception during session destroy for edge [{}] with session id [{}]",
session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId(), e);
}
}
@ -631,25 +656,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void cleanupZombieSessions() {
try {
List<EdgeId> toRemove = new ArrayList<>();
for (EdgeGrpcSession session : sessions.values()) {
if (session instanceof KafkaEdgeGrpcSession kafkaSession &&
!kafkaSession.isConnected() &&
kafkaSession.getConsumer() != null &&
kafkaSession.getConsumer().getConsumer() != null &&
!kafkaSession.getConsumer().getConsumer().isStopped()) {
toRemove.add(kafkaSession.getEdge().getId());
}
}
for (EdgeId edgeId : toRemove) {
log.info("[{}] Destroying session for edge because edge is not connected", edgeId);
EdgeGrpcSession removed = sessions.get(edgeId);
if (removed instanceof KafkaEdgeGrpcSession kafkaSession) {
if (kafkaSession.destroy()) {
sessions.remove(edgeId);
}
}
}
tryToDestroyZombieSessions(getZombieSessions(sessions.values()), s -> sessions.remove(s.getEdge().getId()));
tryToDestroyZombieSessions(getZombieSessions(sessionsById.values()), s -> sessionsById.remove(s.getSessionId()));
zombieSessions.removeIf(zombie -> {
if (zombie.destroy()) {
log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].",
@ -666,4 +675,38 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
}
}
private List<EdgeGrpcSession> getZombieSessions(Collection<EdgeGrpcSession> sessions) {
List<EdgeGrpcSession> result = new ArrayList<>();
for (EdgeGrpcSession session : sessions) {
if (isKafkaSessionAndZombie(session)) {
result.add(session);
}
}
return result;
}
private void tryToDestroyZombieSessions(List<EdgeGrpcSession> sessionsToRemove, Function<EdgeGrpcSession, EdgeGrpcSession> removeFunc) {
for (EdgeGrpcSession toRemove : sessionsToRemove) {
log.info("[{}] Destroying session for edge because edge is not connected", toRemove.getEdge().getId());
if (toRemove.destroy()) {
removeFunc.apply(toRemove);
}
}
}
private boolean isKafkaSessionAndZombie(EdgeGrpcSession session) {
if (session instanceof KafkaEdgeGrpcSession kafkaSession) {
log.debug("[{}] kafkaSession.isConnected() = {}, kafkaSession.getConsumer().getConsumer().isStopped() = {}",
kafkaSession.getEdge().getId(),
kafkaSession.isConnected(),
kafkaSession.getConsumer() != null ? kafkaSession.getConsumer().getConsumer() != null ? kafkaSession.getConsumer().getConsumer().isStopped() : null : null);
return !kafkaSession.isConnected() &&
kafkaSession.getConsumer() != null &&
kafkaSession.getConsumer().getConsumer() != null &&
!kafkaSession.getConsumer().getConsumer().isStopped();
}
return false;
}
}

27
application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java

@ -101,8 +101,21 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
@Override
public ListenableFuture<Boolean> processEdgeEvents() {
if (!isConnected() || isSyncInProgress() || isHighPriorityProcessing) {
log.warn("[{}][{}] Session is not ready (connected={}, syncInProgress={}, highPriority={}), skip starting edge event consumer",
tenantId, edge != null ? edge.getId() : null, isConnected(), isSyncInProgress(), isHighPriorityProcessing);
return Futures.immediateFuture(Boolean.FALSE);
}
if (consumer == null || (consumer.getConsumer() != null && consumer.getConsumer().isStopped())) {
try {
if (consumerExecutor != null && !consumerExecutor.isShutdown()) {
try {
consumerExecutor.shutdown();
awaitConsumerTermination();
} catch (Exception e) {
log.warn("[{}][{}] Failed to shutdown previous consumer executor", tenantId, edge.getId(), e);
}
}
this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer"));
this.consumer = QueueConsumerManager.<TbProtoQueueMsg<ToEdgeEventNotificationMsg>>builder()
.name("TB Edge events [" + edge.getId() + "]")
@ -133,6 +146,7 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
public boolean destroy() {
try {
if (consumer != null) {
log.info("[{}][{}] Stopping edge event consumer...", tenantId, edge != null ? edge.getId() : null);
consumer.stop();
}
} catch (Exception e) {
@ -141,16 +155,25 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
}
consumer = null;
try {
if (consumerExecutor != null) {
if (consumerExecutor != null && !consumerExecutor.isShutdown()) {
consumerExecutor.shutdown();
awaitConsumerTermination();
}
} catch (Exception e) {
log.warn("[{}][{}] Failed to shutdown consumer executor", tenantId, edge.getId(), e);
log.warn("[{}][{}] Failed to shutdown edge event consumer executor", tenantId, edge.getId(), e);
return false;
}
return true;
}
private void awaitConsumerTermination() {
try {
consumerExecutor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS);
} catch (InterruptedException ie) {
log.warn("[{}][{}] Interrupted while awaiting consumer executor termination", tenantId, edge.getId());
}
}
@Override
public void cleanUp() {
String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edge.getId()).getTopic();

8
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java

@ -63,12 +63,12 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor {
Dashboard savedDashboard = edgeCtx.getDashboardService().saveDashboard(dashboard, false);
updateDashboardAssignments(tenantId, dashboardById, savedDashboard, newAssignedCustomers);
updateDashboardAssignments(tenantId, customerId, dashboardById, savedDashboard, newAssignedCustomers);
return created;
}
private void updateDashboardAssignments(TenantId tenantId, Dashboard dashboardById, Dashboard savedDashboard, Set<ShortCustomerInfo> newAssignedCustomers) {
private void updateDashboardAssignments(TenantId tenantId, CustomerId edgeCustomerId, Dashboard dashboardById, Dashboard savedDashboard, Set<ShortCustomerInfo> newAssignedCustomers) {
Set<ShortCustomerInfo> currentAssignedCustomers = new HashSet<>();
if (dashboardById != null) {
if (dashboardById.getAssignedCustomers() != null) {
@ -76,7 +76,7 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor {
}
}
newAssignedCustomers = filterNonExistingCustomers(tenantId, currentAssignedCustomers, newAssignedCustomers);
newAssignedCustomers = filterNonExistingCustomers(tenantId, edgeCustomerId, currentAssignedCustomers, newAssignedCustomers);
Set<CustomerId> addedCustomerIds = new HashSet<>();
Set<CustomerId> removedCustomerIds = new HashSet<>();
@ -100,6 +100,6 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor {
}
}
protected abstract Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers);
protected abstract Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, CustomerId customerId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers);
}

21
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
@ -38,8 +39,10 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
@Component
@ -127,14 +130,24 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor implements Da
}
@Override
protected Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers) {
newAssignedCustomers.addAll(currentAssignedCustomers);
return newAssignedCustomers;
protected Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, CustomerId edgeCustomerId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers) {
boolean edgeCustomerPresentInNewAssignments = newAssignedCustomers.stream()
.map(ShortCustomerInfo::getCustomerId)
.anyMatch(edgeCustomerId::equals);
if (edgeCustomerPresentInNewAssignments) {
Set<ShortCustomerInfo> result = new HashSet<>(newAssignedCustomers);
result.addAll(currentAssignedCustomers);
return result;
} else {
return currentAssignedCustomers.stream()
.filter(info -> !edgeCustomerId.equals(info.getCustomerId()))
.collect(Collectors.toSet());
}
}
@Override
public EdgeEventType getEdgeEventType() {
return EdgeEventType.DASHBOARD;
}
}

3
application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmStatusSubCtx.java

@ -95,6 +95,9 @@ public class TbAlarmStatusSubCtx extends TbAbstractSubCtx {
private void handleAlarmStatusSubscriptionUpdate(TbSubscription<AlarmSubscriptionUpdate> sub, AlarmSubscriptionUpdate subscriptionUpdate) {
try {
AlarmInfo alarm = subscriptionUpdate.getAlarm();
if (!alarm.getOriginator().equals(subscription.getEntityId())) {
return;
}
Set<UUID> alarmsIds = subscription.getAlarmIds();
if (alarmsIds.contains(alarm.getId().getId())) {
if (!subscription.matches(alarm) || subscriptionUpdate.isAlarmDeleted()) {

8
application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java

@ -150,10 +150,10 @@ public class DefaultUpdateService implements UpdateService {
.build());
}
ObjectNode edgeRequest = JacksonUtil.newObjectNode().put(VERSION_PARAM, version);
String edgeInstallVersion = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/installMapping", new HttpEntity<>(edgeRequest.toString(), headers), String.class);
if (edgeInstallVersion != null) {
edgeInstallInstructionsService.setAppVersion(edgeInstallVersion);
edgeUpgradeInstructionsService.setAppVersion(edgeInstallVersion);
String edgePlatformVersion = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/installMapping", new HttpEntity<>(edgeRequest.toString(), headers), String.class);
if (edgePlatformVersion != null) {
edgeInstallInstructionsService.setPlatformEdgeVersion(edgePlatformVersion);
edgeUpgradeInstructionsService.setPlatformEdgeVersion(edgePlatformVersion);
}
EdgeUpgradeMessage edgeUpgradeMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/upgradeMapping", new HttpEntity<>(edgeRequest.toString(), headers), EdgeUpgradeMessage.class);
if (edgeUpgradeMessage != null) {

75
application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java

@ -0,0 +1,75 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.utils;
import lombok.NonNull;
import org.apache.commons.lang3.math.NumberUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import static org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry.DEFAULT_VERSION;
public class CalculatedFieldArgumentUtils {
public static ArgumentEntry transformSingleValueArgument(@NonNull KvEntry kvEntry) {
return kvEntry.getValue() != null ? ArgumentEntry.createSingleValueArgument(kvEntry) : new SingleValueArgumentEntry();
}
public static TsKvEntry createDefaultTsKvEntry(Argument argument, long ts) {
return new BasicTsKvEntry(ts, createDefaultKvEntry(argument), DEFAULT_VERSION);
}
public static AttributeKvEntry createDefaultAttributeEntry(Argument argument, long ts) {
return new BaseAttributeKvEntry(createDefaultKvEntry(argument), ts, DEFAULT_VERSION);
}
private static KvEntry createDefaultKvEntry(Argument argument) {
String key = argument.getRefEntityKey().getKey();
String defaultValue = argument.getDefaultValue();
if (StringUtils.isBlank(defaultValue)) {
return new StringDataEntry(key, null);
}
if (NumberUtils.isParsable(defaultValue)) {
return new DoubleDataEntry(key, Double.parseDouble(defaultValue));
}
if ("true".equalsIgnoreCase(defaultValue) || "false".equalsIgnoreCase(defaultValue)) {
return new BooleanDataEntry(key, Boolean.parseBoolean(defaultValue));
}
return new StringDataEntry(key, defaultValue);
}
public static CalculatedFieldState createStateByType(CalculatedFieldCtx ctx) {
return switch (ctx.getCfType()) {
case SIMPLE -> new SimpleCalculatedFieldState(ctx.getArgNames());
case SCRIPT -> new ScriptCalculatedFieldState(ctx.getArgNames());
};
}
}

87
application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java

@ -45,6 +45,7 @@ import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest {
@ -570,6 +571,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
@Test
public void testScriptCalculatedFieldWhenUsedLatestTsInScript() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
long ts = System.currentTimeMillis() - 300000L;
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts)));
@ -606,6 +608,91 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
});
}
@Test
public void testSimpleCalculatedFieldWhenUseLatestTsIsTrueAndDefaultArguments() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(testDevice.getId());
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("a + b + c");
calculatedField.setDebugSettings(DebugSettings.all());
calculatedField.setConfigurationVersion(1);
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument1 = new Argument();
ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("a", ArgumentType.TS_LATEST, null);
argument1.setRefEntityKey(refEntityKey1);
argument1.setDefaultValue("100");
Argument argument2 = new Argument();
ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("b", ArgumentType.TS_LATEST, null);
argument2.setRefEntityKey(refEntityKey2);
argument2.setDefaultValue("200");
Argument argument3 = new Argument();
ReferencedEntityKey refEntityKey3 = new ReferencedEntityKey("c", ArgumentType.TS_LATEST, null);
argument3.setRefEntityKey(refEntityKey3);
argument3.setDefaultValue("300");
config.setArguments(Map.of("a", argument1, "b", argument2, "c", argument3));
config.setExpression("a + b + c");
Output output = new Output();
output.setName("d");
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(0);
config.setOutput(output);
config.setUseLatestTs(true);
calculatedField.setConfiguration(config);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
await().alias("create CF -> perform initial calculation with default arguments").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode d = getLatestTelemetry(testDevice.getId(), "d");
assertThat(d).isNotNull();
assertThat(d.get("d").get(0).get("value").asText()).isEqualTo("600");
});
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":10}"));
await().alias("update telemetry -> save result with ts of 'a' argument").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode keys = getLatestTelemetry(testDevice.getId(), "d", "a");
assertThat(keys).isNotNull();
String aTs = keys.get("a").get(0).get("ts").asText();
assertThat(keys.get("d").get(0).get("ts").asText()).isEqualTo(aTs);
assertThat(keys.get("d").get(0).get("value").asText()).isEqualTo("510");
});
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"b\":20}"));
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"c\":30}"));
await().alias("update telemetry -> save result with latest ts of updated arguments").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode keys = getLatestTelemetry(testDevice.getId(), "d");
assertThat(keys).isNotNull();
assertThat(keys.get("d").get(0).get("value").asText()).isEqualTo("60");
});
String latestTs = getLatestTelemetry(testDevice.getId(), "d").get("d").get(0).get("ts").asText();
doDelete("/api/plugins/telemetry/DEVICE/" + testDevice.getId() + "/timeseries/delete?keys=b&deleteAllDataForKeys=true").andExpect(status().isOk());
await().alias("delete telemetry -> save result with previous latest ts and default argument").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode keys = getLatestTelemetry(testDevice.getId(), "d");
assertThat(keys).isNotNull();
assertThat(keys.get("d").get(0).get("ts").asText()).isEqualTo(latestTs);
assertThat(keys.get("d").get(0).get("value").asText()).isEqualTo("240");
});
}
@Test
public void testSimpleCalculatedFieldWhenCtxBecameUninitialized() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");

24
application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java

@ -1278,7 +1278,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
@Test
public void testGetEdgeUpgradeInstructions() throws Exception {
// UpdateInfo config is updating from Thingsboard Update server
// UpdateInfo config is updating from the Thingsboard Update server
HashMap<String, EdgeUpgradeInfo> upgradeInfoHashMap = new HashMap<>();
upgradeInfoHashMap.put("3.6.0", new EdgeUpgradeInfo(true, "3.6.1"));
upgradeInfoHashMap.put("3.6.1", new EdgeUpgradeInfo(true, "3.6.2"));
@ -1301,35 +1301,31 @@ public class EdgeControllerTest extends AbstractControllerTest {
// Test 3.5.0 Edge - upgrade not available
String body = "{\"edgeVersion\": \"V_3_5_0\"}";
doPostAsync("/api/plugins/telemetry/EDGE/" + savedEdge.getId().getId() + "/attributes/SERVER_SCOPE", body, String.class, status().isOk());
edgeUpgradeInstructionsService.setAppVersion("3.6.0");
edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.0");
Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.2");
edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.2");
Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.2.7");
edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.2.7");
Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
// Test 3.6.0 Edge - upgrade available
body = "{\"edgeVersion\": \"V_3_6_0\"}";
doPostAsync("/api/plugins/telemetry/EDGE/" + savedEdge.getId().getId() + "/attributes/SERVER_SCOPE", body, String.class, status().isOk());
edgeUpgradeInstructionsService.setAppVersion("3.6.0");
edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.0");
Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.1.5");
edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.1.5");
Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.1.6-SNAPSHOT");
Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.2");
edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.2");
Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
// Test 3.6.1 Edge - upgrade available
body = "{\"edgeVersion\": \"V_3_6_1\"}";
doPostAsync("/api/plugins/telemetry/EDGE/" + savedEdge.getId().getId() + "/attributes/SERVER_SCOPE", body, String.class, status().isOk());
edgeUpgradeInstructionsService.setAppVersion("3.6.1");
edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.1");
Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.2");
Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.2-SNAPSHOT");
edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.2");
Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.2.6");
edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.2.6");
Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
}

52
application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java

@ -34,6 +34,7 @@ import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@ -58,6 +59,7 @@ import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.subscription.SubscriptionErrorCode;
import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope;
@ -478,6 +480,56 @@ public class WebsocketApiTest extends AbstractControllerTest {
Assert.assertFalse(alarmStatusUpdate5.isActive());
}
@Test
public void testAlarmStatusWsCmdForPropagatedAlarms() throws Exception {
loginTenantAdmin();
Device device = new Device();
device.setName("Test device");
device.setLabel("Label");
device.setType("default");
device = doPost("/api/device", device, Device.class);
Asset asset = new Asset();
asset.setName("My asset");
asset.setType("default");
asset = doPost("/api/asset", asset, Asset.class);
EntityRelation entityRelation = new EntityRelation(asset.getId(), device.getId(), "CONTAINS");
doPost("/api/relation", entityRelation);
AlarmStatusCmd cmd = new AlarmStatusCmd(1, asset.getId(), null, List.of(AlarmSeverity.CRITICAL));
getWsClient().send(cmd);
AlarmStatusUpdate update = JacksonUtil.fromString(getWsClient().waitForReply(), AlarmStatusUpdate.class);
Assert.assertEquals(1, update.getCmdId());
Assert.assertFalse(update.isActive());
//create alarm
getWsClient().registerWaitForUpdate();
Alarm alarm = Alarm.builder()
.originator(device.getId())
.severity(AlarmSeverity.CRITICAL)
.type("test_type")
.propagate(true)
.build();
alarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(alarm);
// check no update for asset
String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg);
// check device
AlarmStatusCmd deviceCmd = new AlarmStatusCmd(2, device.getId(), null, List.of(AlarmSeverity.CRITICAL));
getWsClient().send(deviceCmd);
AlarmStatusUpdate deviceUpdate = JacksonUtil.fromString(getWsClient().waitForReply(), AlarmStatusUpdate.class);
Assert.assertEquals(2, deviceUpdate.getCmdId());
Assert.assertTrue(deviceUpdate.isActive());
}
@Test
public void testAlarmStatusWsCmdWithMaxAlarmsCacheSize() throws Exception {
loginTenantAdmin();

37
application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java

@ -27,11 +27,15 @@ import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EdgeConfiguration;
import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
@ -182,6 +186,22 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
customer.setTitle("Edge Customer");
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
// assign edge to customer
edgeImitator.expectMessageAmount(2);
doPost("/api/customer/" + savedCustomer.getUuidId() + "/edge/" + edge.getUuidId(), Edge.class);
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<EdgeConfiguration> edgeConfigurationOpt = edgeImitator.findMessageByType(EdgeConfiguration.class);
Assert.assertTrue(edgeConfigurationOpt.isPresent());
EdgeConfiguration edgeConfiguration = edgeConfigurationOpt.get();
Assert.assertEquals(savedCustomer.getUuidId().getMostSignificantBits(), edgeConfiguration.getCustomerIdMSB());
Assert.assertEquals(savedCustomer.getUuidId().getLeastSignificantBits(), edgeConfiguration.getCustomerIdLSB());
Optional<CustomerUpdateMsg> customerUpdateOpt = edgeImitator.findMessageByType(CustomerUpdateMsg.class);
Assert.assertTrue(customerUpdateOpt.isPresent());
CustomerUpdateMsg customerUpdateMsg = customerUpdateOpt.get();
Customer customerMsg = JacksonUtil.fromString(customerUpdateMsg.getEntity(), Customer.class, true);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUpdateMsg.getMsgType());
Assert.assertEquals(savedCustomer, customerMsg);
Dashboard dashboard = buildDashboardForUplinkMsg(savedCustomer);
// create dashboard on edge
@ -224,6 +244,23 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
foundDashboard = doGet("/api/dashboard/" + dashboard.getUuidId(), Dashboard.class);
Assert.assertEquals(DASHBOARD_TITLE + " Updated", foundDashboard.getName());
// unassign edge from customer
edgeImitator.expectMessageAmount(2);
doDelete("/api/customer/edge/" + edge.getUuidId(), Edge.class);
Assert.assertTrue(edgeImitator.waitForMessages());
edgeConfigurationOpt = edgeImitator.findMessageByType(EdgeConfiguration.class);
Assert.assertTrue(edgeConfigurationOpt.isPresent());
edgeConfiguration = edgeConfigurationOpt.get();
Assert.assertEquals(
new CustomerId(EntityId.NULL_UUID),
new CustomerId(new UUID(edgeConfiguration.getCustomerIdMSB(), edgeConfiguration.getCustomerIdLSB())));
customerUpdateOpt = edgeImitator.findMessageByType(CustomerUpdateMsg.class);
Assert.assertTrue(customerUpdateOpt.isPresent());
customerUpdateMsg = customerUpdateOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, customerUpdateMsg.getMsgType());
Assert.assertEquals(savedCustomer.getUuidId().getMostSignificantBits(), customerUpdateMsg.getIdMSB());
Assert.assertEquals(savedCustomer.getUuidId().getLeastSignificantBits(), customerUpdateMsg.getIdLSB());
}
@Test

2
common/data/pom.xml

@ -113,7 +113,7 @@
<scope>compile</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
</dependency>
</dependencies>

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java

@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoNullChar;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
@ -64,6 +65,7 @@ public final class AiModel extends BaseData<AiModelId> implements HasTenantId, H
@NotBlank
@NoNullChar
@Length(min = 1, max = 255)
@NoXss
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,

2
common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.util.List;
@ -42,6 +43,7 @@ public class OAuth2Client extends BaseDataWithAdditionalInfo<OAuth2ClientId> imp
private TenantId tenantId;
@Schema(description = "Oauth2 client title")
@NotBlank
@NoXss
@Length(fieldName = "title", max = 100, message = "cannot be longer than 100 chars")
private String title;
@Schema(description = "Config for mapping OAuth2 log in response to platform entities", requiredMode = Schema.RequiredMode.REQUIRED)

15
common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java

@ -136,12 +136,25 @@ public class EdgeGrpcClient implements EdgeRpcClient {
.setConnectRequestMsg(ConnectRequestMsg.newBuilder()
.setEdgeRoutingKey(edgeKey)
.setEdgeSecret(edgeSecret)
.setEdgeVersion(EdgeVersion.V_4_2_0)
.setEdgeVersion(getNewestEdgeVersion())
.setMaxInboundMessageSize(maxInboundMessageSize)
.build())
.build());
}
public static EdgeVersion getNewestEdgeVersion() {
EdgeVersion newest = null;
for (EdgeVersion v : EdgeVersion.values()) {
if (v == EdgeVersion.V_LATEST || v == EdgeVersion.UNRECOGNIZED) {
continue;
}
if (newest == null || v.getNumber() > newest.getNumber()) {
newest = v;
}
}
return newest;
}
private StreamObserver<ResponseMsg> initOutputStream(String edgeKey,
Consumer<UplinkResponseMsg> onUplinkResponse,
Consumer<EdgeConfiguration> onEdgeUpdate,

4
common/queue/pom.xml

@ -68,6 +68,10 @@
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</dependency>
<dependency>
<groupId>at.yawk.lz4</groupId>
<artifactId>lz4-java</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-pubsub</artifactId>

5
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java

@ -105,6 +105,7 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
public static final String CLAIMING = "claiming";
public static final String ATTRIBUTE = "attribute";
public static final String RPC_RESPONSE = "Rpc response";
public static final String CONNECT = "connect";
public static final String ATTRIBUTES_REQUEST = "attributes request";
protected final MqttTransportContext context;
@ -257,12 +258,14 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
protected void processOnConnect(MqttPublishMessage msg, String deviceName, String deviceType) {
log.trace("[{}][{}][{}] onDeviceConnect: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName);
int msgId = getMsgId(msg);
AtomicBoolean ackSent = new AtomicBoolean(false);
process(onDeviceConnect(deviceName, deviceType),
result -> {
ack(msg, MqttReasonCodes.PubAck.SUCCESS);
log.trace("[{}][{}][{}] onDeviceConnectOk: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName);
},
t -> logDeviceCreationError(t, deviceName));
t -> processFailure(msgId, deviceName, CONNECT, ackSent, t));
}
public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional<DeviceProfile> deviceProfileOpt) {

2
dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryCompositeKey.java

@ -25,7 +25,7 @@ import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class KeyDictionaryCompositeKey implements Serializable{
public class KeyDictionaryCompositeKey implements Serializable {
@Transient
private static final long serialVersionUID = -4089175869616037523L;

7
dao/src/main/java/org/thingsboard/server/dao/model/sqlts/dictionary/KeyDictionaryEntry.java

@ -36,8 +36,7 @@ public final class KeyDictionaryEntry {
@Column(name = KEY_COLUMN)
private String key;
@Column(name = KEY_ID_COLUMN, unique = true, columnDefinition = "int")
@Generated
private int keyId;
@Column(name = KEY_ID_COLUMN, unique = true, columnDefinition = "int", insertable = false, updatable = false)
private Integer keyId;
}
}

68
dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java

@ -17,8 +17,6 @@ package org.thingsboard.server.dao.sqlts.dictionary;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@ -48,43 +46,34 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@Override
public Integer getOrSaveKeyId(String strKey) {
Integer keyId = keyDictionaryMap.get(strKey);
if (keyId == null) {
Optional<KeyDictionaryEntry> tsKvDictionaryOptional;
tsKvDictionaryOptional = keyDictionaryRepository.findById(new KeyDictionaryCompositeKey(strKey));
if (tsKvDictionaryOptional.isEmpty()) {
creationLock.lock();
try {
keyId = keyDictionaryMap.get(strKey);
if (keyId != null) {
return keyId;
}
tsKvDictionaryOptional = keyDictionaryRepository.findById(new KeyDictionaryCompositeKey(strKey));
if (tsKvDictionaryOptional.isEmpty()) {
KeyDictionaryEntry keyDictionaryEntry = new KeyDictionaryEntry();
keyDictionaryEntry.setKey(strKey);
try {
KeyDictionaryEntry saved = keyDictionaryRepository.save(keyDictionaryEntry);
keyDictionaryMap.put(saved.getKey(), saved.getKeyId());
keyId = saved.getKeyId();
} catch (DataIntegrityViolationException | ConstraintViolationException e) {
tsKvDictionaryOptional = keyDictionaryRepository.findById(new KeyDictionaryCompositeKey(strKey));
KeyDictionaryEntry dictionary = tsKvDictionaryOptional.orElseThrow(() -> new RuntimeException("Failed to get KeyDictionaryEntry entity from DB!"));
keyDictionaryMap.put(dictionary.getKey(), dictionary.getKeyId());
keyId = dictionary.getKeyId();
}
} else {
keyId = tsKvDictionaryOptional.get().getKeyId();
}
} finally {
creationLock.unlock();
}
} else {
keyId = tsKvDictionaryOptional.get().getKeyId();
keyDictionaryMap.put(strKey, keyId);
Integer cached = keyDictionaryMap.get(strKey);
if (cached != null) {
return cached;
}
var compositeKey = new KeyDictionaryCompositeKey(strKey);
Optional<Integer> existingId = keyDictionaryRepository.findById(compositeKey).map(KeyDictionaryEntry::getKeyId);
if (existingId.isPresent()) {
return cacheAndReturn(strKey, existingId.get());
}
creationLock.lock();
try {
Integer fromCache = keyDictionaryMap.get(strKey);
if (fromCache != null) {
return fromCache;
}
Integer keyId = keyDictionaryRepository.upsertAndGetKeyId(strKey);
if (keyId != null) {
return cacheAndReturn(strKey, keyId);
}
log.warn("upsertAndGetKeyId returned: [{}] for key: [{}], falling back to findById", keyId, strKey);
keyId = keyDictionaryRepository.findById(compositeKey)
.map(KeyDictionaryEntry::getKeyId)
.orElseThrow(() -> new IllegalStateException(
"Failed to resolve keyId for string key: " + strKey + " after fallback."));
return cacheAndReturn(strKey, keyId);
} finally {
creationLock.unlock();
}
return keyId;
}
@Override
@ -98,4 +87,9 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService
return DaoUtil.pageToPageData(keyDictionaryRepository.findAll(DaoUtil.toPageable(pageLink)));
}
private Integer cacheAndReturn(String key, Integer keyId) {
keyDictionaryMap.put(key, keyId);
return keyId;
}
}

6
dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryRepository.java

@ -19,6 +19,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryCompositeKey;
import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry;
@ -31,4 +32,7 @@ public interface KeyDictionaryRepository extends JpaRepository<KeyDictionaryEntr
@Query("SELECT e FROM KeyDictionaryEntry e ORDER BY e.keyId ASC")
Page<KeyDictionaryEntry> findAll(Pageable pageable);
}
@Query(value = "INSERT INTO key_dictionary (key) VALUES (:key) ON CONFLICT (key) DO UPDATE SET key = EXCLUDED.key RETURNING key_id", nativeQuery = true)
Integer upsertAndGetKeyId(@Param("key") String key);
}

111
dao/src/test/java/org/thingsboard/server/dao/sqlts/dictionary/KeyDictionaryDaoTest.java

@ -0,0 +1,111 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sqlts.dictionary;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.dao.dictionary.KeyDictionaryDao;
import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryCompositeKey;
import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry;
import org.thingsboard.server.dao.service.AbstractServiceTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
@DaoSqlTest
public class KeyDictionaryDaoTest extends AbstractServiceTest {
@Autowired
private KeyDictionaryDao keyDictionaryDao;
@Autowired
private KeyDictionaryRepository keyDictionaryRepository;
private static final String KEY = "testKeyDictionaryDaoTestKey";
@Test
public void testGetOrSaveKeyId_concurrent() throws Exception {
int threads = 8;
ExecutorService executor = Executors.newFixedThreadPool(threads);
CountDownLatch allReady = new CountDownLatch(threads);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch allDone = new CountDownLatch(threads);
Integer[] keyIds = new Integer[threads];
try {
for (int i = 0; i < threads; i++) {
final int idx = i;
executor.submit(() -> {
allReady.countDown();
try {
// wait until all threads are ready
start.await();
// concurrent call
Integer id = keyDictionaryDao.getOrSaveKeyId(KEY);
keyIds[idx] = id;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
allDone.countDown();
}
});
}
// ensure all threads are queued
allReady.await(5, TimeUnit.SECONDS);
// fire the start gun
start.countDown();
// wait for all to finish
allDone.await(10, TimeUnit.SECONDS);
} finally {
executor.shutdownNow();
}
// basic sanity
for (int i = 0; i < threads; i++) {
assertThat(keyIds[i])
.as("keyId[%s]", i)
.isNotNull()
.isGreaterThan(0);
}
// all threads must see the same keyId
int first = keyIds[0];
assertThat(first).isGreaterThan(0);
assertThat(Arrays.stream(keyIds).distinct().count())
.as("all threads should get the same keyId")
.isEqualTo(1);
// DB must have exactly one row for this key and the same id
KeyDictionaryCompositeKey id = new KeyDictionaryCompositeKey(KEY);
Optional<KeyDictionaryEntry> entry = keyDictionaryRepository.findById(id);
assertThat(entry.isPresent()).isTrue();
assertThat(entry.get().getKeyId()).isEqualTo(first);
keyDictionaryRepository.deleteById(id);
}
}

2
msa/js-executor/package.json

@ -15,7 +15,7 @@
"dependencies": {
"config": "^4.1.1",
"express": "^5.1.0",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"kafkajs": "^2.2.4",
"long": "^5.3.2",
"uuid-parse": "^1.1.0",

14
msa/js-executor/yarn.lock

@ -945,10 +945,10 @@ isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
js-yaml@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
dependencies:
argparse "^2.0.1"
@ -1533,9 +1533,9 @@ supports-preserve-symlinks-flag@^1.0.0:
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tar-fs@^2.0.0, tar-fs@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.3.tgz#fb3b8843a26b6f13a08e606f7922875eb1fbbf92"
integrity sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==
version "2.1.4"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930"
integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==
dependencies:
chownr "^1.1.1"
mkdirp-classic "^0.5.2"

2
msa/web-ui/package.json

@ -19,7 +19,7 @@
"express": "^5.1.0",
"http": "0.0.0",
"http-proxy": "^1.18.1",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},

4
msa/web-ui/server.ts

@ -81,12 +81,12 @@ let connections: Socket[] = [];
}
}
});
app.all('/api/*', (req, res) => {
app.all('/api/*splat', (req, res) => {
logger.debug(req.method + ' ' + req.originalUrl);
apiProxy.web(req, res);
});
app.all('/static/rulenode/*', (req, res) => {
app.all('/static/rulenode/*splat', (req, res) => {
apiProxy.web(req, res);
});

14
msa/web-ui/yarn.lock

@ -1017,10 +1017,10 @@ isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
js-yaml@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
dependencies:
argparse "^2.0.1"
@ -1615,9 +1615,9 @@ supports-preserve-symlinks-flag@^1.0.0:
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tar-fs@^2.0.0, tar-fs@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.3.tgz#fb3b8843a26b6f13a08e606f7922875eb1fbbf92"
integrity sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==
version "2.1.4"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930"
integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==
dependencies:
chownr "^1.1.1"
mkdirp-classic "^0.5.2"

32
pom.xml

@ -63,7 +63,7 @@
<curator.version>5.6.0</curator.version>
<zookeeper.version>3.9.3</zookeeper.version>
<protobuf.version>3.25.5</protobuf.version> <!-- A Major v4 does not support by the pubsub yet-->
<grpc.version>1.68.1</grpc.version>
<grpc.version>1.76.0</grpc.version>
<tbel.version>1.2.8</tbel.version>
<lombok.version>1.18.38</lombok.version>
<paho.client.version>1.2.5</paho.client.version>
@ -86,6 +86,7 @@
org.apache.kafka.common.network.NetworkReceive class in the application module. It addresses the issue https://issues.apache.org/jira/browse/KAFKA-4090.
Here is the source to track https://github.com/apache/kafka/tree/trunk/clients/src/main/java/org/apache/kafka/common/network -->
<kafka.version>3.9.1</kafka.version>
<lz4.version>1.10.1</lz4.version> <!-- to fix CVE-2025-12183 and CVE-2025-66566 introduced through kafka-clients 3.9.1 TODO: remove when kafka-clients is bumped -->
<bucket4j.version>8.10.1</bucket4j.version>
<antlr.version>3.5.3</antlr.version>
<snakeyaml.version>2.2</snakeyaml.version>
@ -101,7 +102,7 @@
<passay.version>1.6.4</passay.version>
<ua-parser.version>1.6.1</ua-parser.version>
<commons-beanutils.version>1.9.4</commons-beanutils.version>
<commons-beanutils.version>1.11.0</commons-beanutils.version>
<commons-collections.version>4.4</commons-collections.version>
<protobuf-dynamic.version>1.0.4TB</protobuf-dynamic.version>
<wire-schema.version>3.7.1</wire-schema.version>
@ -110,7 +111,7 @@
<jakarta.el.version>4.0.2</jakarta.el.version>
<antisamy.version>1.7.5</antisamy.version>
<snmp4j.version>3.8.0</snmp4j.version>
<langchain4j.version>1.1.0</langchain4j.version>
<langchain4j.version>1.8.0-TB</langchain4j.version>
<error_prone_annotations.version>2.38.0</error_prone_annotations.version>
<animal-sniffer-annotations.version>1.24</animal-sniffer-annotations.version>
<auto-value-annotations.version>1.11.0</auto-value-annotations.version>
@ -146,7 +147,7 @@
<firebase-admin.version>9.2.0</firebase-admin.version>
<snappy.version>1.1.10.5</snappy.version>
<rocksdbjni.version>9.10.0</rocksdbjni.version>
<netty.version>4.1.125.Final</netty.version> <!-- to fix CVEs. TODO: remove when fixed in spring-boot-dependencies -->
<netty.version>4.1.128.Final</netty.version> <!-- to fix CVEs. TODO: remove when fixed in spring-boot-dependencies -->
</properties>
<modules>
@ -911,7 +912,7 @@
<scope>import</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>${langchain4j.version}</version>
<type>pom</type>
@ -1153,6 +1154,17 @@
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>${kafka.version}</version>
<exclusions>
<exclusion>
<groupId>org.lz4</groupId>
<artifactId>lz4-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>at.yawk.lz4</groupId>
<artifactId>lz4-java</artifactId>
<version>${lz4.version}</version> <!-- to fix CVE introduced through kafka-clients 3.9.1 -->
</dependency>
<dependency>
<groupId>com.github.springtestdbunit</groupId>
@ -1433,6 +1445,12 @@
<groupId>org.apache.cassandra</groupId>
<artifactId>cassandra-all</artifactId>
<version>${cassandra-all.version}</version>
<exclusions>
<exclusion>
<groupId>org.lz4</groupId>
<artifactId>lz4-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.testng</groupId>
@ -1908,6 +1926,10 @@
<id>central</id>
<url>https://repo1.maven.org/maven2/</url>
</repository>
<repository>
<id>thingsboard-repo</id>
<url>https://repo.thingsboard.io/artifactory/libs-release-public</url>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>

2
rule-engine/rule-engine-api/pom.xml

@ -99,7 +99,7 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
<dependency>

6
rule-engine/rule-engine-components/pom.xml

@ -96,6 +96,10 @@
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</dependency>
<dependency>
<groupId>at.yawk.lz4</groupId>
<artifactId>lz4-java</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sns</artifactId>
@ -154,7 +158,7 @@
<artifactId>json-path</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
</dependencies>

4
tools/pom.xml

@ -55,6 +55,10 @@
<groupId>org.apache.cassandra</groupId>
<artifactId>cassandra-all</artifactId>
</dependency>
<dependency>
<groupId>at.yawk.lz4</groupId>
<artifactId>lz4-java</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>

5
ui-ngx/package.json

@ -141,6 +141,9 @@
"rollup": "4.22.4",
"@babel/core": "7.25.2",
"esbuild": "0.23.0",
"jquery.terminal/coveralls-next/form-data": "4.0.4"
"jquery.terminal/coveralls-next/form-data": "4.0.4",
"node-forge": "1.3.3",
"glob": "10.5.0",
"path-to-regexp": "0.1.12"
}
}

66
ui-ngx/patches/@angular+common+18.2.13.patch

@ -0,0 +1,66 @@
diff --git a/node_modules/@angular/common/esm2022/http/src/xsrf.mjs b/node_modules/@angular/common/esm2022/http/src/xsrf.mjs
index da69c17..d17f6ad 100755
--- a/node_modules/@angular/common/esm2022/http/src/xsrf.mjs
+++ b/node_modules/@angular/common/esm2022/http/src/xsrf.mjs
@@ -19,6 +19,10 @@ export const XSRF_HEADER_NAME = new InjectionToken(ngDevMode ? 'XSRF_HEADER_NAME
providedIn: 'root',
factory: () => XSRF_DEFAULT_HEADER_NAME,
});
+/**
+ * Regex to match absolute URLs, including protocol-relative URLs.
+ */
+const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i;
/**
* Retrieves the current XSRF token to use with the next outgoing request.
*
@@ -69,7 +73,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
args: [XSRF_COOKIE_NAME]
}] }] });
export function xsrfInterceptorFn(req, next) {
- const lcUrl = req.url.toLowerCase();
// Skip both non-mutating requests and absolute URLs.
// Non-mutating requests don't require a token, and absolute URLs require special handling
// anyway as the cookie set
@@ -77,8 +80,7 @@ export function xsrfInterceptorFn(req, next) {
if (!inject(XSRF_ENABLED) ||
req.method === 'GET' ||
req.method === 'HEAD' ||
- lcUrl.startsWith('http://') ||
- lcUrl.startsWith('https://')) {
+ ABSOLUTE_URL_REGEX.test(req.url)) {
return next(req);
}
const token = inject(HttpXsrfTokenExtractor).getToken();
diff --git a/node_modules/@angular/common/fesm2022/http.mjs b/node_modules/@angular/common/fesm2022/http.mjs
index 1655480..d1dbb38 100755
--- a/node_modules/@angular/common/fesm2022/http.mjs
+++ b/node_modules/@angular/common/fesm2022/http.mjs
@@ -2352,6 +2352,10 @@ const XSRF_HEADER_NAME = new InjectionToken(ngDevMode ? 'XSRF_HEADER_NAME' : '',
providedIn: 'root',
factory: () => XSRF_DEFAULT_HEADER_NAME,
});
+/**
+ * Regex to match absolute URLs, including protocol-relative URLs.
+ */
+const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i;
/**
* Retrieves the current XSRF token to use with the next outgoing request.
*
@@ -2402,7 +2406,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
args: [XSRF_COOKIE_NAME]
}] }] });
function xsrfInterceptorFn(req, next) {
- const lcUrl = req.url.toLowerCase();
// Skip both non-mutating requests and absolute URLs.
// Non-mutating requests don't require a token, and absolute URLs require special handling
// anyway as the cookie set
@@ -2410,8 +2413,7 @@ function xsrfInterceptorFn(req, next) {
if (!inject(XSRF_ENABLED) ||
req.method === 'GET' ||
req.method === 'HEAD' ||
- lcUrl.startsWith('http://') ||
- lcUrl.startsWith('https://')) {
+ ABSOLUTE_URL_REGEX.test(req.url)) {
return next(req);
}
const token = inject(HttpXsrfTokenExtractor).getToken();

88
ui-ngx/patches/@angular+compiler+18.2.13.patch

@ -0,0 +1,88 @@
diff --git a/node_modules/@angular/compiler/fesm2022/compiler.mjs b/node_modules/@angular/compiler/fesm2022/compiler.mjs
index a00b189..260e7be 100755
--- a/node_modules/@angular/compiler/fesm2022/compiler.mjs
+++ b/node_modules/@angular/compiler/fesm2022/compiler.mjs
@@ -18631,6 +18631,7 @@ function SECURITY_SCHEMA() {
'area|ping',
'audio|src',
'a|href',
+ 'a|xlink:href',
'a|ping',
'blockquote|cite',
'body|background',
@@ -18644,6 +18645,75 @@ function SECURITY_SCHEMA() {
'track|src',
'video|poster',
'video|src',
+
+ // MathML namespace
+ // https://crsrc.org/c/third_party/blink/renderer/core/sanitizer/sanitizer.cc;l=753-768;drc=b3eb16372dcd3317d65e9e0265015e322494edcd;bpv=1;bpt=1
+ 'annotation|href',
+ 'annotation|xlink:href',
+ 'annotation-xml|href',
+ 'annotation-xml|xlink:href',
+ 'maction|href',
+ 'maction|xlink:href',
+ 'malignmark|href',
+ 'malignmark|xlink:href',
+ 'math|href',
+ 'math|xlink:href',
+ 'mroot|href',
+ 'mroot|xlink:href',
+ 'msqrt|href',
+ 'msqrt|xlink:href',
+ 'merror|href',
+ 'merror|xlink:href',
+ 'mfrac|href',
+ 'mfrac|xlink:href',
+ 'mglyph|href',
+ 'mglyph|xlink:href',
+ 'msub|href',
+ 'msub|xlink:href',
+ 'msup|href',
+ 'msup|xlink:href',
+ 'msubsup|href',
+ 'msubsup|xlink:href',
+ 'mmultiscripts|href',
+ 'mmultiscripts|xlink:href',
+ 'mprescripts|href',
+ 'mprescripts|xlink:href',
+ 'mi|href',
+ 'mi|xlink:href',
+ 'mn|href',
+ 'mn|xlink:href',
+ 'mo|href',
+ 'mo|xlink:href',
+ 'mpadded|href',
+ 'mpadded|xlink:href',
+ 'mphantom|href',
+ 'mphantom|xlink:href',
+ 'mrow|href',
+ 'mrow|xlink:href',
+ 'ms|href',
+ 'ms|xlink:href',
+ 'mspace|href',
+ 'mspace|xlink:href',
+ 'mstyle|href',
+ 'mstyle|xlink:href',
+ 'mtable|href',
+ 'mtable|xlink:href',
+ 'mtd|href',
+ 'mtd|xlink:href',
+ 'mtr|href',
+ 'mtr|xlink:href',
+ 'mtext|href',
+ 'mtext|xlink:href',
+ 'mover|href',
+ 'mover|xlink:href',
+ 'munder|href',
+ 'munder|xlink:href',
+ 'munderover|href',
+ 'munderover|xlink:href',
+ 'semantics|href',
+ 'semantics|xlink:href',
+ 'none|href',
+ 'none|xlink:href',
]);
registerContext(SecurityContext.RESOURCE_URL, [
'applet|code',

4
ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts

@ -56,9 +56,7 @@ export class CheckConnectivityDialogComponent extends DialogComponent<CheckConne
},
chatModelConfig: {
modelType: ModelType.CHAT,
provider: this.data.AIModel.configuration.provider,
providerConfig: {...this.data.AIModel.configuration.providerConfig},
modelId: this.data.AIModel.configuration.modelId,
...this.data.AIModel.configuration,
maxRetries: 0,
timeoutSeconds: 20
}

16
ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, Inject, OnInit, ViewChild } from '@angular/core';
import { Component, Inject, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -51,7 +51,7 @@ export interface AlarmDetailsDialogData {
templateUrl: './alarm-details-dialog.component.html',
styleUrls: ['./alarm-details-dialog.component.scss']
})
export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDialogComponent, boolean> implements OnInit {
export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDialogComponent, boolean> {
alarmId: string;
alarmFormGroup: UntypedFormGroup;
@ -128,13 +128,12 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
this.alarmFormGroup.get('startTime')
.patchValue(this.datePipe.transform(alarm.startTs, 'yyyy-MM-dd HH:mm:ss'));
}
if (alarm.startTs || alarm.endTs) {
if (alarm.startTs || alarm.clearTs) {
let duration = '';
if (alarm.startTs && (alarm.status === AlarmStatus.ACTIVE_ACK || alarm.status === AlarmStatus.ACTIVE_UNACK)) {
if (alarm.startTs && !alarm.cleared) {
duration = this.millisecondsToTimeStringPipe.transform(Date.now() - alarm.startTs);
}
if (alarm.endTs && (alarm.status === AlarmStatus.CLEARED_ACK || alarm.status === AlarmStatus.CLEARED_UNACK)) {
duration = this.millisecondsToTimeStringPipe.transform(alarm.endTs - alarm.startTs);
} else if (alarm.clearTs && alarm.cleared) {
duration = this.millisecondsToTimeStringPipe.transform(alarm.clearTs - alarm.startTs);
}
this.alarmFormGroup.get('duration').patchValue(duration);
}
@ -144,9 +143,6 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
this.alarmFormGroup.get('alarmDetails').patchValue(alarm.details);
}
ngOnInit(): void {
}
close(): void {
this.dialogRef.close(this.alarmUpdated);
}

4
ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts

@ -236,9 +236,9 @@ export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = P
this.table = null;
}
updateData(closeDetails = false) {
updateData(closeDetails = false, reloadEntity = true) {
if (this.table) {
this.table.updateData(closeDetails);
this.table.updateData(closeDetails, reloadEntity);
} else if (this.entityDetailsPage) {
this.entityDetailsPage.reload();
}

2
ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts

@ -68,7 +68,7 @@ export interface IEntitiesTableComponent {
addEnabled(): boolean;
clearSelection(): void;
updateData(closeDetails?: boolean): void;
updateData(closeDetails?: boolean, reloadEntity?: boolean): void;
onRowClick($event: Event, entity): void;
toggleEntityDetails($event: Event, entity);
addEntity($event: Event): void;

2
ui-ngx/src/app/modules/home/pages/asset/asset.component.ts

@ -100,6 +100,6 @@ export class AssetComponent extends EntityComponent<AssetInfo> {
}
onAssetProfileUpdated() {
this.entitiesTableConfig.updateData(false);
this.entitiesTableConfig.updateData(false, false);
}
}

2
ui-ngx/src/app/modules/home/pages/device/device.component.ts

@ -142,7 +142,7 @@ export class DeviceComponent extends EntityComponent<DeviceInfo> {
}
onDeviceProfileUpdated() {
this.entitiesTableConfig.updateData(false);
this.entitiesTableConfig.updateData(false, false);
}
onDeviceProfileChanged(deviceProfile: DeviceProfileInfo) {

18
ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html

@ -30,39 +30,39 @@
<mat-tab-group [(selectedIndex)]="tabIndex" (selectedTabChange)="selectedTabChange(tabIndex)">
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="mdi:ubuntu"></mat-icon>
Ubuntu
<mat-icon class="tabs-icon" svgIcon="docker"></mat-icon>
Docker
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border">
<div class="tb-form-panel no-padding stroked">
<tb-markdown [data]='contentData.ubuntu'></tb-markdown>
<tb-markdown [data]='contentData.docker'></tb-markdown>
</div>
</div>
</ng-template>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="mdi:centos"></mat-icon>
CentOS/RHEL
<mat-icon class="tabs-icon" svgIcon="mdi:ubuntu"></mat-icon>
Ubuntu
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border">
<div class="tb-form-panel no-padding stroked">
<tb-markdown [data]='contentData.centos'></tb-markdown>
<tb-markdown [data]='contentData.ubuntu'></tb-markdown>
</div>
</div>
</ng-template>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="docker"></mat-icon>
Docker
<mat-icon class="tabs-icon" svgIcon="mdi:centos"></mat-icon>
CentOS/RHEL
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border">
<div class="tb-form-panel no-padding stroked">
<tb-markdown [data]='contentData.docker'></tb-markdown>
<tb-markdown [data]='contentData.centos'></tb-markdown>
</div>
</div>
</ng-template>

2
ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts

@ -176,7 +176,7 @@ export class MobileAppComponent extends EntityComponent<MobileApp> {
context: ctx,
showCloseButton: false,
popoverContentStyle: {padding: '16px 24px'},
isModal: false
isModal: true
});
releaseNotesPanelPopover.tbComponentRef.instance.popover = releaseNotesPanelPopover;
releaseNotesPanelPopover.tbComponentRef.instance.editorContentApplied.subscribe((releaseNotes) => {

30
ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts

@ -59,9 +59,33 @@ export class EditorPanelComponent implements OnInit {
resize: false,
setup: (editor) => {
editor.on('PostRender', function() {
const container = editor.getContainer().closest('.tb-popover-content');
const uiContainer = document.querySelector('.tox.tox-tinymce-aux');
container.parentNode.appendChild(uiContainer);
const container = document.querySelector('.tox.tox-tinymce-aux');
const styleSheet = document.createElement('style');
styleSheet.innerText = `
.tox-tiered-menu .tox-menu {
width: fit-content;
max-width: min(80%, 440px);
@media screen and (max-width: 510px) {
max-width: calc(100% - 64px);
}
media screen and (min-width: 511px) and (max-width: 548px) {
max-width: calc(100% - 84px);
}
media screen and (min-width: 549px) and (max-width: 599px) {
max-width: calc(100% - 104px);
}
}
.tox-tiered-menu .tox-menu .tox-collection__item-label {
word-break: normal;
}
@media screen and (max-width: 890px) {
.tox-tiered-menu > .tox-collection--list:not(:first-child) {
left: auto !important;
right: 0 !important;
}
}
`;
container.prepend(styleSheet);
});
},
relative_urls: false,

2
ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html

@ -70,6 +70,7 @@
<ng-container *ngIf="targetNotificationForm.get('configuration.usersFilter.filterByTenants').value; else tenantProfiles">
<tb-entity-list
formControlName="tenantsIds"
syncIdsWithDB
subscriptSizing="dynamic"
labelText="{{ 'tenant.tenants' | translate }}"
placeholderText="{{ 'tenant.tenants' | translate }}"
@ -80,6 +81,7 @@
<ng-template #tenantProfiles>
<tb-entity-list
formControlName="tenantProfilesIds"
syncIdsWithDB
subscriptSizing="dynamic"
labelText="{{ 'tenant-profile.tenant-profiles' | translate }}"
placeholderText="{{ 'tenant-profile.tenant-profiles' | translate }}"

31
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts

@ -95,6 +95,37 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
autofocus: false,
branding: false,
promotion: false,
setup: (editor) => {
editor.on('PostRender', function() {
const container = document.querySelector('.tox.tox-tinymce-aux');
const styleSheet = document.createElement('style');
styleSheet.innerText = `
.tox-tiered-menu .tox-menu {
width: fit-content;
max-width: min(80%, 440px);
@media screen and (max-width: 510px) {
max-width: calc(100% - 64px);
}
media screen and (min-width: 511px) and (max-width: 548px) {
max-width: calc(100% - 84px);
}
media screen and (min-width: 549px) and (max-width: 599px) {
max-width: calc(100% - 104px);
}
}
.tox-tiered-menu .tox-menu .tox-collection__item-label {
word-break: normal;
}
@media screen and (max-width: 890px) {
.tox-tiered-menu > .tox-collection--list:not(:first-child) {
left: auto !important;
right: 0 !important;
}
}
`;
container.prepend(styleSheet);
});
},
relative_urls: false,
urlconverter_callback: (url) => url
};

2
ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts

@ -101,6 +101,6 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
}
onTenantProfileUpdated() {
this.entitiesTableConfig.updateData(false);
this.entitiesTableConfig.updateData(false, false);
}
}

9
ui-ngx/src/app/shared/models/ai-model.models.ts

@ -110,6 +110,7 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
'o3',
'o3-mini',
'o1',
'gpt-5.1',
'gpt-5',
'gpt-5-mini',
'gpt-5-nano',
@ -135,6 +136,7 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
AiProvider.GOOGLE_AI_GEMINI,
{
modelList: [
'gemini-3-pro-preview',
'gemini-2.5-pro',
'gemini-2.5-flash',
'gemini-2.5-flash-lite',
@ -149,6 +151,7 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
AiProvider.GOOGLE_VERTEX_AI_GEMINI,
{
modelList: [
'gemini-3-pro-preview',
'gemini-2.5-pro',
'gemini-2.5-flash',
'gemini-2.5-flash-lite',
@ -181,12 +184,10 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
AiProvider.ANTHROPIC,
{
modelList: [
'claude-opus-4-5',
'claude-opus-4-1',
'claude-opus-4-0',
'claude-sonnet-4-5',
'claude-sonnet-4-0',
'claude-3-7-sonnet-latest',
'claude-3-5-haiku-latest',
'claude-haiku-4-5'
],
providerFieldsList: ['apiKey'],
modelFieldsList: ['temperature', 'topP', 'topK', 'maxOutputTokens'],

4
ui-ngx/src/app/shared/models/edge.models.ts

@ -184,9 +184,9 @@ export interface EdgeInstructions {
}
export enum EdgeInstructionsMethod {
docker,
ubuntu,
centos,
docker
centos
}
export const edgeVersionAttributeKey = 'edgeVersion';

515
ui-ngx/src/assets/locale/locale.constant-da_DK.json

@ -875,14 +875,14 @@
"data-points-storage-days": "Opbevaringsdage for datapunkter",
"device-api": "Enheds-API",
"email": "E-mail",
"email-messages": "E-mailbeskeder",
"email-messages-daily-activity": "Daglig aktivitet for e-mailbeskeder",
"email-messages-monthly-activity": "Månedlig aktivitet for e-mailbeskeder",
"email-messages": "E-mail-beskeder",
"email-messages-daily-activity": "Daglig aktivitet for e-mail-beskeder",
"email-messages-monthly-activity": "Månedlig aktivitet for e-mail-beskeder",
"executions": "Udførelser",
"scripts": "Scripts",
"scripts-hourly-activity": "Timebaseret aktivitet for scripts",
"scripts-daily-activity": "Daglig aktivitet for scripts",
"scripts-monthly-activity": "Månedlig aktivitet for scripts",
"scripts-hourly-activity": "Scriptaktivitet pr. time",
"scripts-daily-activity": "Scriptaktivitet pr. dag",
"scripts-monthly-activity": "Scriptaktivitet pr. måned",
"javascript": "JavaScript",
"javascript-executions": "JavaScript-udførelser",
"tbel": "TBEL",
@ -1002,15 +1002,15 @@
"type-sms-sent": "SMS sendt"
},
"debug-settings": {
"label": "Fejlsøgningskonfiguration",
"label": "Fejlfindingskonfiguration",
"on-failure": "Kun fejl (24/7)",
"all-messages": "Alle beskeder ({{time}})",
"failures": "Fejl",
"entity": "enhed",
"hint": {
"main-limited": "Alle {{entity}} fejlsøgningsbeskeder vil blive hastighedsbegrænset, med maksimalt {{msg}} beskeder tilladt pr. {{time}}.",
"on-failure": "Gem alle fejlsøgningshændelser uden tidsbegrænsning.",
"all-messages": "Gem alle fejlsøgningshændelser i en tidsbegrænset periode."
"main-limited": "Der vil blive logget højst {{msg}} {{entity}} fejlfindingsbeskeder pr. {{time}}.",
"on-failure": "Log kun fejlbeskeder.",
"all-messages": "Log alle fejlfindingsbeskeder."
}
},
"calculated-fields": {
@ -1111,13 +1111,15 @@
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
"github-models": "GitHub-modeller"
"github-models": "GitHub-modeller",
"ollama": "Ollama"
},
"name-required": "Navn er påkrævet.",
"name-max-length": "Navn må højst være 255 tegn.",
"provider": "Udbyder",
"api-key": "API-nøgle",
"api-key-required": "API-nøgle er påkrævet.",
"api-key-open-ai-required": "API-nøgle er påkrævet ved brug af den officielle OpenAI API.",
"project-id": "Projekt-ID",
"project-id-required": "Projekt-ID er påkrævet",
"location": "Placering",
@ -1152,19 +1154,36 @@
"presence-penalty": "Tilstedeværelsesstraf",
"presence-penalty-hint": "Påfører en fast straf på sandsynligheden for et token, hvis det allerede er optrådt i teksten.",
"frequency-penalty": "Frekvensstraf",
"frequency-penalty-hint": "Påfører en straf på et tokens sandsynlighed, som stiger baseret på dets hyppighed i teksten.",
"max-output-tokens": "Maksimalt outputtokens",
"max-output-tokens-min": "Skal være større end 0.",
"max-output-tokens-hint": "Angiver det maksimale antal tokens, som modellen kan generere i ét svar.",
"frequency-penalty-hint": "Anvender en straf på sandsynligheden for et token, som stiger afhængigt af dets hyppighed i teksten.",
"max-output-tokens": "Maksimalt antal output-tokens",
"max-output-tokens-hint": "Angiver det maksimale antal tokens, som \nmodellen kan generere i ét svar.",
"context-length": "Kontekstens længde",
"context-length-hint": "Definerer størrelsen på kontekstvinduet i tokens. Denne værdi angiver den samlede hukommelsesgrænse for modellen, inklusiv både brugerens input og det genererede svar.",
"endpoint": "Endpoint",
"endpoint-required": "Endpoint er påkrævet.",
"baseurl": "Base-URL",
"baseurl-required": "Base-URL er påkrævet.",
"service-version": "Serviceversion",
"check-connectivity": "Tjek forbindelsen",
"check-connectivity-success": "Testanmodningen var vellykket",
"check-connectivity-success": "Testanmodningen blev gennemført med succes",
"check-connectivity-failed": "Testanmodningen mislykkedes",
"no-model-matching": "Ingen modeller, der matcher '{{entity}}', blev fundet.",
"model-required": "Model er påkrævet.",
"no-model-text": "Ingen modeller fundet."
"no-model-text": "Ingen modeller fundet.",
"authentication": "Godkendelse",
"authentication-basic-hint": "Bruger standard HTTP Basic-godkendelse. Brugernavn og adgangskode kombineres, Base64-kodes og sendes i en \"Authorization\"-header med hver anmodning til Ollama-serveren.",
"authentication-token-hint": "Bruger Bearer-token-godkendelse. Det angivne token sendes direkte i en \"Authorization\"-header med hver anmodning til Ollama-serveren.",
"authentication-type": {
"none": "Ingen",
"basic": "Basic",
"token": "Token"
},
"username": "Brugernavn",
"username-required": "Brugernavn er påkrævet.",
"password": "Adgangskode",
"password-required": "Adgangskode er påkrævet.",
"token": "Token",
"token-required": "Token er påkrævet."
},
"confirm-on-exit": {
"message": "Du har ikke gemte ændringer. Er du sikker på, at du vil forlade denne side?",
@ -2237,12 +2256,12 @@
"tqs": "TQS: Både TCP og SMS aktive; TCP i køtilstand, SMS i standardtilstand (ikke understøttet fra LWM2M 1.1)",
"sq": "SQ: SMS i køtilstand (ikke understøttet fra LWM2M 1.1)"
},
"binding-tooltip": "Angiver understøttede bindingstyper i klienten (/1/x/7). Bør matche værdien i “Supported Binding and Modes” i enhedsobjektet (/3/0/16). Kun én transportbinding kan bruges per session.",
"binding-tooltip": "Dette er listen i \"binding\"-ressourcen for LwM2M-serverobjektet – /1/x/7.\nAngiver de understøttede bindingstilstande i LwM2M-klienten.\nDenne værdi BØR være den samme som værdien i ressourcen \"Supported Binding and Modes\" i enhedsobjektet (/3/0/16).\nSelvom flere transportmetoder understøttes, kan kun én transportbinding anvendes under hele transportsessionen.\nFor eksempel, når både UDP og SMS understøttes, kan LwM2M-klienten og LwM2M-serveren vælge at kommunikere enten via UDP eller SMS under hele transportsessionen.",
"bootstrap-server": "Bootstrap-server",
"lwm2m-server": "LwM2M-server",
"include-bootstrap-server": "Inkluder Bootstrap Server-opdateringer",
"bootstrap-update-title": "Du har allerede konfigureret Bootstrap-server. Er du sikker på, at du vil udelukke opdateringerne?",
"bootstrap-update-text": "Vær forsigtig, efter bekræftelse vil Bootstrap Server-konfigurationen ikke kunne gendannes.",
"include-bootstrap-server": "Inkludér opdateringer til Bootstrap-server",
"bootstrap-update-title": "Du har allerede konfigureret en Bootstrap-server. Er du sikker på, at du vil udelukke opdateringerne?",
"bootstrap-update-text": "Vær opmærksom på, at efter bekræftelse vil konfigurationsdataene for Bootstrap-serveren ikke kunne gendannes.",
"server-host": "Host",
"server-host-required": "Host er påkrævet.",
"server-port": "Port",
@ -4308,8 +4327,8 @@
"current-password": "Nuværende adgangskode",
"copy-jwt-token": "Kopiér JWT-token",
"jwt-token": "JWT-token",
"token-valid-till": "Token er gyldig til",
"tokenCopiedSuccessMessage": "JWT-token er kopieret til udklipsholderen",
"token-valid-till": "Token er gyldig indtil",
"tokenCopiedSuccessMessage": "JWT-token er blevet kopieret til udklipsholderen",
"tokenCopiedWarnMessage": "JWT-token er udløbet! Opdater venligst siden."
},
"profiles": {
@ -4476,17 +4495,23 @@
"title-max-length": "Titel skal være mindre end 256 tegn",
"type": {
"jks": "JKS",
"js-module": "JS modul",
"lwm2m-model": "LWM2M model",
"pkcs-12": "PKCS #12"
"js-module": "JS-modul",
"lwm2m-model": "LWM2M-model",
"pkcs-12": "PKCS #12",
"general": "Generelt"
},
"resource-sub-type": "Undertype",
"sub-type": {
"image": "billede",
"scada-symbol": "Scada symbol",
"scada-symbol": "SCADA-symbol",
"extension": "Udvidelse",
"module": "Modul"
}
},
"resource-is-in-use": "Ressourcen bruges af andre entiteter",
"resources-are-in-use": "Ressourcerne bruges af andre entiteter",
"resource-is-in-use-text": "Ressourcen <b>'{{title}}'</b> blev ikke slettet, fordi den bruges af følgende entiteter:",
"resources-are-in-use-text": "Ikke alle ressourcer blev slettet, fordi de bruges af andre entiteter.</br>Du kan se de refererede entiteter ved at klikke på knappen <b>Referencer</b> i den tilsvarende ressource-række.</br>Hvis du stadig ønsker at slette disse ressourcer, skal du vælge dem i tabellen nedenfor og klikke på knappen <b>Slet valgte</b>.",
"delete-resource-in-use-text": "Hvis du stadig ønsker at slette ressourcen, skal du klikke på knappen <b>Slet alligevel</b>."
},
"javascript": {
"add": "Tilføj JavaScript-ressource",
@ -4700,13 +4725,13 @@
"customer-cache-expiration-hint": "Angiver maksimumsinterval for gemte kundedata. 0 = aldrig udløber.",
"customer-cache-expiration-required": "Cache udløbstid for kunder er påkrævet.",
"customer-cache-expiration-range": "Udløbstiden skal være ≥ 0.",
"interval-start": "Interval start",
"interval-end": "Interval slut",
"interval-start": "Start af interval",
"interval-end": "Intervalslut",
"time-unit": "Tidsenhed",
"fetch-mode": "Hentetilstand",
"fetch-mode": "Hentemåde",
"order-by-timestamp": "Sorter efter tidsstempel",
"limit": "Grænse",
"limit-hint": "Min: 2, maks: 1000. Vælg 'First' eller 'Last' for én post.",
"limit-hint": "Min. grænseværdi er 2, maks. 1000. Hvis du vil hente én enkelt post, skal du vælge hentemåde 'Første' eller 'Sidste'.",
"limit-required": "Grænse er påkrævet.",
"limit-range": "Grænsen skal være mellem 2 og 1000.",
"time-unit-milliseconds": "Millisekunder",
@ -4714,9 +4739,9 @@
"time-unit-minutes": "Minutter",
"time-unit-hours": "Timer",
"time-unit-days": "Dage",
"time-value-range": "Værdien skal være mellem 1 og 2147483647.",
"start-interval-value-required": "Interval start er påkrævet.",
"end-interval-value-required": "Interval slut er påkrævet.",
"time-value-range": "Tilladt område er fra 1 til 2147483647.",
"start-interval-value-required": "Intervalstart er påkrævet.",
"end-interval-value-required": "Intervalslut er påkrævet.",
"filter": "Filter",
"switch": "Skift",
"math-templatization-tooltip": "Understøtter templatisering via $[messageKey] og ${metadataKey}.",
@ -4759,7 +4784,7 @@
"copy-from": "Kopiér fra",
"data-to-metadata": "Data til metadata",
"metadata-to-data": "Metadata til data",
"use-regular-expression-hint": "Brug regulært udtryk for at kopiere nøgler efter mønster.\n\nTips:\nTryk 'Enter' for at bekræfte feltinput.\nTryk 'Backspace' for at slette felt. Flere felter understøttes.",
"use-regular-expression-hint": "Brug regulært udtryk til at kopiere nøgler efter mønster.\n\nTips og tricks:\nTryk på 'Enter' for at fuldføre indtastning af feltnavn.\nTryk på 'Backspace' for at slette feltnavn. Flere feltnavne understøttes.",
"interval": "Interval",
"interval-required": "Interval er påkrævet",
"interval-hint": "Dedupliceringsinterval i sekunder.",
@ -4812,7 +4837,7 @@
"latest-telemetry": "Seneste telemetri",
"add-telemetry-key": "Tilføj telemetrinøgle",
"delete-from": "Slet fra",
"use-regular-expression-delete-hint": "Brug regulært udtryk til sletning efter mønster.\n\nTips:\nTryk 'Enter' for at bekræfte.\nTryk 'Backspace' for at slette.",
"use-regular-expression-delete-hint": "Brug regulært udtryk til at slette nøgler efter mønster.\n\nTips og tricks:\nTryk på 'Enter' for at afslutte feltindtastning.\nTryk på 'Backspace' for at slette feltnavn.\nFlere feltnavne understøttes.",
"fetch-into": "Hent til",
"attr-mapping": "Attributmapping:",
"source-attribute": "Kildeattribut",
@ -4823,11 +4848,11 @@
"target-key-required": "Målnøgle er påkrævet.",
"attr-mapping-required": "Mindst én mapping skal angives.",
"fields-mapping": "Feltermapping",
"fields-mapping-hint": "Brug $entityId for at gemme afsenderens id.",
"fields-mapping-hint": "Hvis beskedfeltet er sat til $entityId, vil oprindelses-id for beskeden blive gemt i den angivne tabelkolonne.",
"relations-query-config-direction-suffix": "afsender",
"profile-name": "Profilnavn",
"fetch-circle-parameter-info-from-metadata-hint": "Metadatafelt '{{perimeterKeyName}}' skal være i formatet: {\"latitude\":48.196, \"longitude\":24.6532, \"radius\":100.0, \"radiusUnit\":\"METER\"}",
"fetch-poligon-parameter-info-from-metadata-hint": "Metadatafelt '{{perimeterKeyName}}' skal være i formatet: [[48.19736,24.65235],...]",
"fetch-circle-parameter-info-from-metadata-hint": "Metadatafeltet '{{perimeterKeyName}}' skal være i følgende format: {\"latitude\":48.196, \"longitude\":24.6532, \"radius\":100.0, \"radiusUnit\":\"METER\"}",
"fetch-poligon-parameter-info-from-metadata-hint": "Metadatafeltet '{{perimeterKeyName}}' skal være i følgende format: [[48.19736,24.65235],[48.19800,24.65060],...,[48.19849,24.65420]]",
"short-templatization-tooltip": "Brug $[messageKey] og ${metadataKey} til at hente værdier.",
"fields-mapping-required": "Mindst én feltmapping er påkrævet.",
"at-least-one-field-required": "Mindst ét inputfelt skal have en værdi.",
@ -4918,8 +4943,8 @@
"mail-body-type": "Indholdstype",
"body-type-template": "Indholdstypemønster",
"reply-routing-configuration": "Svarrute-konfiguration",
"rpc-reply-routing-configuration-hint": "Konfiguration for at identificere tjeneste, session og anmodning i metadata.",
"reply-routing-configuration-hint": "Konfiguration for at identificere tjeneste og anmodning i metadata.",
"rpc-reply-routing-configuration-hint": "Disse konfigurationsparametre angiver metadata-nøglenavne, der bruges til at identificere tjenesten, sessionen og anmodningen til at sende et svar tilbage.",
"reply-routing-configuration-hint": "Disse konfigurationsparametre angiver metadata-nøglenavne, der bruges til at identificere tjenesten og anmodningen til at sende et svar tilbage.",
"request-id-metadata-attribute": "Anmodnings-id",
"service-id-metadata-attribute": "Tjeneste-id",
"session-id-metadata-attribute": "Session-id",
@ -4947,7 +4972,7 @@
"value-required": "Værdi er påkrævet",
"topic-pattern": "Emnemønster",
"key-pattern": "Nøglemønster",
"key-pattern-hint": "Valgfri. Angiv partition eller nøgle til brug ved sending.",
"key-pattern-hint": "Valgfrit. Hvis et gyldigt partitionsnummer er angivet, vil det blive brugt ved afsendelse af posten. Hvis ingen partition er angivet, vil nøglen blive brugt i stedet. Hvis ingen af delene er angivet, tildeles en partition i en round-robin-rækkefølge.",
"topic-pattern-required": "Emnemønster er påkrævet",
"topic": "Emne",
"topic-required": "Emne er påkrævet",
@ -4956,18 +4981,18 @@
"other-properties": "Andre egenskaber",
"key": "Nøgle",
"key-required": "Nøgle er påkrævet",
"retries": "Antal genforsøg ved fejl",
"min-retries-message": "Kun 0 genforsøg tilladt som minimum",
"batch-size-bytes": "Batchstørrelse i bytes",
"min-batch-size-bytes-message": "Minimum batchstørrelse er 0",
"linger-ms": "Buffer-tid lokalt (ms)",
"min-linger-ms-message": "Minimum 0 ms tilladt",
"buffer-memory-bytes": "Maks. bufferstørrelse (bytes)",
"min-buffer-memory-message": "Minimum bufferstørrelse er 0",
"memory-buffer-size-range": "Bufferstørrelse skal være mellem 0 og {{max}} KB",
"acks": "Antal kvitteringer",
"topic-arn-pattern": "ARN-emnemønster",
"topic-arn-pattern-required": "ARN-emnemønster er påkrævet",
"retries": "Antal automatiske forsøg ved fejl",
"min-retries-message": "Kun 0 som minimumsantal forsøg er tilladt.",
"batch-size-bytes": "Batchstørrelse ved afsendelse (i bytes)",
"min-batch-size-bytes-message": "Kun 0 som minimum batchstørrelse er tilladt.",
"linger-ms": "Tidsbuffer lokalt (ms)",
"min-linger-ms-message": "Kun 0 ms som minimumværdi er tilladt.",
"buffer-memory-bytes": "Maksimal klientbufferstørrelse (i bytes)",
"min-buffer-memory-message": "Kun 0 som minimum bufferstørrelse er tilladt.",
"memory-buffer-size-range": "Bufferstørrelsen skal være mellem 0 og {{max}} KB",
"acks": "Antal bekræftelser",
"topic-arn-pattern": "Emne-ARN-mønster",
"topic-arn-pattern-required": "Emne-ARN-mønster er påkrævet",
"aws-access-key-id": "AWS Access Key ID",
"aws-access-key-id-required": "AWS Access Key ID er påkrævet",
"aws-secret-access-key": "AWS Secret Access Key",
@ -4987,35 +5012,35 @@
"password": "Adgangskode",
"automatic-recovery": "Automatisk gendannelse",
"connection-timeout-ms": "Forbindelsestimeout (ms)",
"min-connection-timeout-ms-message": "Minimum værdi er 0 ms",
"handshake-timeout-ms": "Handshake-timeout (ms)",
"min-handshake-timeout-ms-message": "Minimum værdi er 0 ms",
"min-connection-timeout-ms-message": "Kun 0 ms som minimumværdi er tilladt.",
"handshake-timeout-ms": "Tidsgrænse for håndtryk (ms)",
"min-handshake-timeout-ms-message": "Kun 0 ms som minimumværdi er tilladt.",
"client-properties": "Klientegenskaber",
"queue-url-pattern": "Kø-URL mønster",
"queue-url-pattern-required": "Kø-URL mønster er påkrævet",
"queue-url-pattern": "Køens URL-mønster",
"queue-url-pattern-required": "Køens URL-mønster er påkrævet",
"delay-seconds": "Forsinkelse (sekunder)",
"min-delay-seconds-message": "Minimum forsinkelse er 0 sekunder",
"max-delay-seconds-message": "Maksimal forsinkelse er 900 sekunder",
"min-delay-seconds-message": "Kun 0 sekunder som minimumværdi er tilladt.",
"max-delay-seconds-message": "Maksimum tilladt værdi er 900 sekunder.",
"name": "Navn",
"name-required": "Navn er påkrævet",
"name-required": "Navn er påkrævet.",
"queue-type": "Køtype",
"sqs-queue-standard": "Standard",
"sqs-queue-fifo": "FIFO",
"gcp-project-id": "GCP-projekt-ID",
"gcp-project-id-required": "GCP-projekt-ID er påkrævet",
"gcp-service-account-key": "GCP servicekonto-nøglefil",
"gcp-service-account-key-required": "GCP servicekonto-nøglefil er påkrævet",
"gcp-service-account-key": "GCP-servicekonto-nøglefil",
"gcp-service-account-key-required": "GCP-servicekonto-nøglefil er påkrævet",
"pubsub-topic-name": "Emnenavn",
"pubsub-topic-name-required": "Emnenavn er påkrævet",
"message-attributes": "Beskedattributter",
"message-attributes-hint": "Brug <code><span style=\"color: #000;\">$&#123;</span>metadataKey<span style=\"color: #000000;\">&#125;</span></code> for værdi fra metadata, <code><span style=\"color: #000;\">$[</span>messageKey<span style=\"color: #000;\">]</span></code> for værdi fra meddelelsesindhold i navn-/værdifelter",
"connect-timeout": "Forbindelsestimeout (sek)",
"connect-timeout-required": "Forbindelsestimeout er påkrævet.",
"connect-timeout-range": "Timeout skal være mellem 1 og 200.",
"message-attributes-hint": "Brug <code><span style=\"color: #000;\">$&#123;</span>metadataKey<span style=\"color: #000000;\">&#125;</span></code> for værdi fra metadata, <code><span style=\"color: #000;\">$[</span>messageKey<span style=\"color: #000;\">]</span></code> for værdi fra beskedens indhold i felterne navn/værdi.",
"connect-timeout": "Forbindelsestidsgrænse (sek.)",
"connect-timeout-required": "Forbindelsestidsgrænse er påkrævet.",
"connect-timeout-range": "Forbindelsestidsgrænsen skal være mellem 1 og 200.",
"client-id": "Klient-ID",
"client-id-hint": "Valgfri. Lad være tomt for automatisk generering. For at undgå konflikter i mikrotjenester, brug unikke ID’er.",
"append-client-id-suffix": "Tilføj service-ID som suffix til klient-ID",
"client-id-suffix-hint": "Valgfrit. Anvendes når \"Client ID\" er angivet eksplicit. Hvis valgt, tilføjes Service ID som et suffiks til Client ID. Hjælper med at undgå fejl, når platformen kører i microservices-tilstand.",
"client-id-hint": "Valgfrit. Lad stå tomt for automatisk genereret Klient-ID. Vær forsigtig ved angivelse af Klient-ID. De fleste MQTT-brokere tillader ikke flere forbindelser med samme Klient-ID. Hvis du forbinder til sådanne brokere, skal din MQTT Klient-ID være unik. Når platformen kører i mikroservicetilstand, startes en kopi af regelnoden i hver mikroservice. Dette vil automatisk føre til flere MQTT-klienter med samme ID og kan forårsage fejl i regelnoden. For at undgå sådanne fejl skal du aktivere \"Tilføj Service-ID som suffiks til Klient-ID\" nedenfor.",
"append-client-id-suffix": "Tilføj Service-ID som suffiks til Klient-ID",
"client-id-suffix-hint": "Valgfrit. Anvendes når \"Klient-ID\" er angivet manuelt. Hvis valgt, tilføjes Service-ID som suffiks til Klient-ID. Hjælper med at undgå fejl, når platformen kører i mikroservicetilstand.",
"device-id": "Enheds-ID",
"device-id-required": "Enheds-ID er påkrævet.",
"clean-session": "Ren session",
@ -5043,23 +5068,23 @@
"private-key-password": "Adgangskode til privat nøgle",
"use-system-smtp-settings": "Brug systemets SMTP-indstillinger",
"use-metadata-dynamic-interval": "Brug dynamisk interval",
"metadata-dynamic-interval-hint": "Intervalstart og -slut felterne understøtter skabeloner. Brug $[messageKey] eller ${metadataKey}.",
"use-metadata-interval-patterns-hint": "Hvis valgt, bruges mønstre fra metadata eller data til interval.",
"metadata-dynamic-interval-hint": "Indtastningsfelterne for intervalstart og -slut understøtter skabelonbrug. Bemærk, at den indsatte skabelonværdi skal være angivet i millisekunder. Brug $[messageKey] for at hente værdi fra beskeden og ${metadataKey} for at hente værdi fra metadata.",
"use-metadata-interval-patterns-hint": "Hvis valgt, bruger regelnoden start- og slutmønstre for interval fra beskedens metadata eller data, forudsat at intervallerne er i millisekunder.",
"use-message-alarm-data": "Brug alarmdata fra besked",
"overwrite-alarm-details": "Overskriv alarmdetaljer",
"use-alarm-severity-pattern": "Brug mønster for alarmens alvorlighed",
"check-all-keys": "Tjek at alle specificerede felter er til stede",
"check-all-keys-hint": "Tjekker at alle nøgler findes i data og metadata.",
"check-relation-to-specific-entity": "Tjek relation til specifik enhed",
"check-relation-to-specific-entity-tooltip": "Tjekker om relation eksisterer til en specifik eller hvilken som helst enhed baseret på type og retning.",
"check-relation-hint": "Tjek for relation til bestemt eller vilkårlig enhed.",
"delete-relation-with-specific-entity": "Slet relation med specifik enhed",
"delete-relation-with-specific-entity-hint": "Sletter relation med en bestemt enhed. Ellers slettes alle matchende relationer.",
"delete-relation-hint": "Sletter relation fra oprindelsen til den angivne enhed eller liste af enheder.",
"use-alarm-severity-pattern": "Brug mønster for alarmens alvorlighedsgrad",
"check-all-keys": "Tjek, at alle angivne felter er til stede",
"check-all-keys-hint": "Hvis valgt, kontrolleres det, at alle angivne nøgler er til stede i beskeddata og metadata.",
"check-relation-to-specific-entity": "Tjek relation til specifik entitet",
"check-relation-to-specific-entity-tooltip": "Hvis aktiveret, kontrolleres tilstedeværelsen af relation til en specifik entitet. Ellers kontrolleres tilstedeværelsen af relation til en vilkårlig entitet. I begge tilfælde foretages relationsopslag baseret på konfigureret retning og type.",
"check-relation-hint": "Kontrollerer eksistensen af relation til en specifik eller vilkårlig entitet baseret på retning og relationstype.",
"delete-relation-with-specific-entity": "Slet relation til specifik entitet",
"delete-relation-with-specific-entity-hint": "Hvis aktiveret, slettes kun relationen til én specifik entitet. Ellers fjernes relationer til alle matchende entiteter.",
"delete-relation-hint": "Sletter relation fra afsenderen af den indgående besked til den angivne entitet eller entitetsliste baseret på retning og type.",
"remove-current-relations": "Fjern nuværende relationer",
"remove-current-relations-hint": "Fjerner nuværende relationer baseret på type og retning.",
"change-originator-to-related-entity": "Skift oprindelse til relateret enhed",
"change-originator-to-related-entity-hint": "Behandler beskeden som om den kommer fra en anden enhed.",
"remove-current-relations-hint": "Fjerner nuværende relationer fra afsenderen af den indgående besked baseret på retning og type.",
"change-originator-to-related-entity": "Skift afsender til relateret entitet",
"change-originator-to-related-entity-hint": "Anvendes til at behandle en besked som om den kommer fra en anden entitet.",
"start-interval": "Interval start",
"end-interval": "Interval slut",
"start-interval-required": "Interval start er påkrævet.",
@ -5119,31 +5144,31 @@
"email-sender": "Email afsender",
"fields-to-check": "Felter der skal kontrolleres",
"add-detail": "Tilføj detalje",
"check-all-keys-tooltip": "Kontrollerer, at alle angivne felter findes i besked og metadata.",
"fields-to-check-hint": "Tryk på \"Enter\" for at afslutte. Flere felter understøttes.",
"entity-details-list-empty": "Mindst én detalje skal vælges.",
"check-all-keys-tooltip": "Hvis aktiveret, kontrolleres tilstedeværelsen af alle felter, der er angivet i besked- og metadatafeltnavne, i den indgående besked og dens metadata.",
"fields-to-check-hint": "Tryk på \"Enter\" for at afslutte indtastning af feltnavn. Flere feltnavne understøttes.",
"entity-details-list-empty": "Mindst ét detaljefelt skal vælges.",
"alarm-status": "Alarmstatus",
"alarm-required": "Mindst én alarmstatus skal vælges.",
"no-entity-details-matching": "Ingen tilsvarende entitetsdetaljer fundet.",
"custom-table-name": "Brugertabelnavn",
"no-entity-details-matching": "Ingen matchende entitetsdetaljer blev fundet.",
"custom-table-name": "Brugertilpasset tabelnavn",
"custom-table-name-required": "Tabelnavn er påkrævet",
"custom-table-hint": "Skal starte med 'cs_tb_'. Angiv uden præfikset.",
"custom-table-hint": "Tabellen skal være oprettet i din Cassandra-klynge, og dens navn skal starte med præfikset 'cs_tb_' for at undgå indsættelse af data i de fælles TB-tabeller. Indtast tabelnavnet her uden 'cs_tb_'-præfikset.",
"message-field": "Beskedfelt",
"message-field-required": "Beskedfelt er påkrævet.",
"table-col": "Tabelkolonne",
"table-col-required": "Tabelkolonne er påkrævet.",
"latitude-field-name": "Navn på breddegradsfelt",
"longitude-field-name": "Navn på længdegradsfelt",
"latitude-field-name-required": "Navn på breddegradsfelt er påkrævet.",
"longitude-field-name-required": "Navn på længdegradsfelt er påkrævet.",
"fetch-perimeter-info-from-metadata": "Hent perimeteroplysninger fra metadata",
"fetch-perimeter-info-from-metadata-tooltip": "Parser metadatafelt for cirkel eller polygon afhængigt af perimeter-typen.",
"perimeter-key-name": "Perimeter nøgle navn",
"perimeter-key-name-hint": "Metadata felt med perimeter info.",
"perimeter-key-name-required": "Perimeter nøgle navn er påkrævet.",
"latitude-field-name": "Feltnavn for breddegrad",
"longitude-field-name": "Feltnavn for længdegrad",
"latitude-field-name-required": "Feltnavn for breddegrad er påkrævet.",
"longitude-field-name-required": "Feltnavn for længdegrad er påkrævet.",
"fetch-perimeter-info-from-metadata": "Hent perimeterinformation fra metadata",
"fetch-perimeter-info-from-metadata-tooltip": "Hvis perimeter-typen er sat til 'Polygon', vil værdien af metadatafeltet '{{perimeterKeyName}}' blive brugt direkte som definitionsværdi uden yderligere parsing. Hvis perimeter-typen er sat til 'Circle', vil værdien af '{{perimeterKeyName}}'-metadatafeltet blive parseret for at udtrække felterne 'latitude', 'longitude', 'radius', og 'radiusUnit', som bruges til at definere cirkelperimeteren.",
"perimeter-key-name": "Perimeter-nøgle",
"perimeter-key-name-hint": "Navn på metadatafelt, der indeholder perimeterinformation.",
"perimeter-key-name-required": "Perimeter-nøgle er påkrævet.",
"perimeter-circle": "Cirkel",
"perimeter-polygon": "Polygon",
"perimeter-type": "Perimeter type",
"perimeter-type": "Perimeter-type",
"circle-center-latitude": "Cirkelens center breddegrad",
"circle-center-latitude-required": "Center breddegrad er påkrævet.",
"circle-center-longitude": "Cirkelens center længdegrad",
@ -5155,25 +5180,25 @@
"range-unit-nautical-mile": "Sømil",
"range-units": "Afstandsenheder",
"range-units-required": "Afstandsenheder er påkrævet.",
"range": "Rækkevidde",
"range-required": "Rækkevidde er påkrævet.",
"range": "Interval",
"range-required": "Interval er påkrævet.",
"polygon-definition": "Polygondefinition",
"polygon-definition-required": "Polygondefinition er påkrævet.",
"polygon-definition-hint": "Format: [[lat1,lon1],[lat2,lon2],...,[latN,lonN]].",
"polygon-definition-hint": "Brug følgende format til manuel definition af polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].",
"min-inside-duration": "Minimal varighed indenfor",
"min-inside-duration-value-required": "Værdi er påkrævet",
"min-inside-duration-value-required": "Minimal varighed indenfor er påkrævet",
"min-inside-duration-time-unit": "Tidsenhed for minimal varighed indenfor",
"min-outside-duration": "Minimal varighed udenfor",
"min-outside-duration-value-required": "Værdi er påkrævet",
"min-outside-duration-value-required": "Minimal varighed udenfor er påkrævet",
"min-outside-duration-time-unit": "Tidsenhed for minimal varighed udenfor",
"tell-failure-if-absent": "Rapporter fejl ved fravær",
"tell-failure-if-absent-hint": "Hvis mindst én af de valgte nøgler ikke findes, vil udgående besked rapportere \"Fejl\".",
"get-latest-value-with-ts": "Hent tidsstempel for de seneste telemetriværdier",
"tell-failure-if-absent": "Rapportér fejl",
"tell-failure-if-absent-hint": "Hvis mindst én valgt nøgle ikke findes, vil den udgående besked rapportere \"Fejl\".",
"get-latest-value-with-ts": "Hent tidsstempel for seneste telemetriværdier",
"get-latest-value-with-ts-hint": "Hvis valgt, vil de seneste telemetriværdier også inkludere tidsstempel, f.eks.: \"temp\": \"&#123;\"ts\":1574329385897, \"value\":42&#125;\"",
"ignore-null-strings": "Ignorér tomme strenge",
"ignore-null-strings-hint": "Ignorerer felter med tomme værdier.",
"add-metadata-key-values-as-kafka-headers": "Tilføj nøgle-værdi par fra metadata til Kafka headers",
"add-metadata-key-values-as-kafka-headers-hint": "Hvis valgt, tilføjes metadataens nøgle-værdi par som byte arrays med foruddefineret tegnkodning.",
"ignore-null-strings-hint": "Hvis valgt, vil regelnoden ignorere entitetsfelter med tom værdi.",
"add-metadata-key-values-as-kafka-headers": "Tilføj metadata nøgle-værdi-par til Kafka header",
"add-metadata-key-values-as-kafka-headers-hint": "Hvis valgt, tilføjes nøgle-værdi-par fra beskedmetadata til headers i udgående Kafka-poster som byte-arrays med foruddefineret tegnkodning.",
"charset-encoding": "Tegnkodning",
"charset-encoding-required": "Tegnkodning er påkrævet.",
"charset-us-ascii": "US-ASCII",
@ -5182,63 +5207,63 @@
"charset-utf-16be": "UTF-16BE",
"charset-utf-16le": "UTF-16LE",
"charset-utf-16": "UTF-16",
"select-queue-hint": "Vælg et kønavn fra rullemenu eller angiv et brugerdefineret navn.",
"device-profile-node-hint": "Nyttigt ved varighed eller gentagne betingelser for at sikre fortsat evaluering af alarmtilstand.",
"persist-alarm-rules": "Gem tilstand for alarmregler",
"persist-alarm-rules-hint": "Gemmer behandlingsstatus i databasen.",
"select-queue-hint": "Kønavnet kan vælges fra en rullemenu eller angives manuelt.",
"device-profile-node-hint": "Nyttigt, hvis du har varigheds- eller gentagelsesbetingelser for at sikre kontinuitet i evalueringen af alarmtilstand.",
"persist-alarm-rules": "Bevar tilstand for alarmregler",
"persist-alarm-rules-hint": "Hvis aktiveret, vil regelnoden gemme behandlingstilstanden i databasen.",
"fetch-alarm-rules": "Hent tilstand for alarmregler",
"fetch-alarm-rules-hint": "Genskaber tilstand ved opstart for at sikre korrekt alarmstatus.",
"input-value-key": "Input nøgle",
"input-value-key-required": "Input nøgle er påkrævet.",
"output-value-key": "Output nøgle",
"output-value-key-required": "Output nøgle er påkrævet.",
"number-of-digits-after-floating-point": "Antal decimaler",
"number-of-digits-after-floating-point-range": "Antal decimaler skal være mellem 0 og 15.",
"failure-if-delta-negative": "Rapportér fejl ved negativ delta",
"failure-if-delta-negative-tooltip": "Fejl ved negativ forskel mellem værdier.",
"fetch-alarm-rules-hint": "Hvis aktiveret, vil regelnoden gendanne behandlingstilstanden ved initialisering og sikre, at alarmer udløses selv efter servergenstart. Ellers vil tilstanden blive gendannet, når den første besked fra enheden modtages.",
"input-value-key": "Inputværdinøgle",
"input-value-key-required": "Inputværdinøgle er påkrævet.",
"output-value-key": "Outputværdinøgle",
"output-value-key-required": "Outputværdinøgle er påkrævet.",
"number-of-digits-after-floating-point": "Antal decimaler efter komma",
"number-of-digits-after-floating-point-range": "Antallet af decimaler skal være mellem 0 og 15.",
"failure-if-delta-negative": "Rapportér fejl, hvis delta er negativ",
"failure-if-delta-negative-tooltip": "Regelnoden tvinger beskedbehandling til at fejle, hvis delta-værdien er negativ.",
"use-caching": "Brug caching",
"use-caching-tooltip": "Regelknuden vil cache værdien af \"{{inputValueKey}}\" fra den indkommende besked for at forbedre ydeevnen. Bemærk, at cachen ikke opdateres, hvis \"{{inputValueKey}}\" ændres et andet sted.",
"use-caching-tooltip": "Regelnoden vil cache værdien af \"{{inputValueKey}}\", som modtages fra den indgående besked, for at forbedre ydeevnen. Bemærk, at cachen ikke opdateres, hvis du ændrer værdien af \"{{inputValueKey}}\" andre steder.",
"add-time-difference-between-readings": "Tilføj tidsforskel mellem aflæsninger af \"{{inputValueKey}}\"",
"add-time-difference-between-readings-tooltip": "Hvis aktiveret, tilføjer regelknuden \"{{periodValueKey}}\" til den udgående besked.",
"period-value-key": "Tidsintervalnøgle",
"period-value-key-required": "Tidsintervalnøgle er påkrævet.",
"general-pattern-hint": "Brug ${metadataKey} for værdi fra metadata, $[messageKey] for værdi fra meddelelsesindhold.",
"alarm-severity-pattern-hint": "Brug <code><span style=\"color: #000;\">$&#123;</span>metadataKey<span style=\"color: #000;\">&#125;</span></code> for værdi fra metadata, <code><span style=\"color: #000;\">$[</span>messageKey<span style=\"color: #000;\">]</span></code> for værdi fra meddelelsesindhold. Alarmens alvorlighed skal være et systemniveau (CRITICAL, MAJOR osv.)",
"output-node-name-hint": "<b>Regelknudens navn</b> svarer til <b>relationstypen</b> for den udgående besked og bruges til at videresende beskeder til andre regelknuder i den kaldende regelkæde.",
"add-time-difference-between-readings-tooltip": "Hvis aktiveret, tilføjer regelnoden \"{{periodValueKey}}\" til den udgående besked.",
"period-value-key": "Periodeværdinøgle",
"period-value-key-required": "Periodeværdinøgle er påkrævet.",
"general-pattern-hint": "Brug ${metadataKey} for værdi fra metadata, $[messageKey] for værdi fra beskedens indhold.",
"alarm-severity-pattern-hint": "Brug <code><span style=\"color: #000;\">$&#123;</span>metadataKey<span style=\"color: #000;\">&#125;</span></code> for værdi fra metadata, <code><span style=\"color: #000;\">$[</span>messageKey<span style=\"color: #000;\">]</span></code> for værdi fra beskedens indhold. Alarmernes alvorlighed skal være systemdefineret (CRITICAL, MAJOR osv.)",
"output-node-name-hint": "<b>Regelnodens navn</b> svarer til <b>relationstypen</b> for den udgående besked og bruges til at videresende beskeder til andre regelnoder i den kaldende regelkæde.",
"use-server-ts": "Brug serverens tidsstempel",
"use-server-ts-hint": "Brug serverens aktuelle tidsstempel for tidsseriedata, der mangler et eksplicit tidsstempel. Dette hjælper med korrekt rækkefølge, når der behandles beskeder fra flere kilder eller ved uordnet ankomst.",
"kv-map-pattern-hint": "Alle inputfelter understøtter skabelonisering. Brug $[messageKey] for værdi fra beskeden og ${metadataKey} for værdi fra metadata.",
"kv-map-single-pattern-hint": "Inputfelt understøtter skabelonisering. Brug $[messageKey] for værdi fra beskeden og ${metadataKey} for værdi fra metadata.",
"use-server-ts-hint": "Brug serverens aktuelle tidsstempel for tidsseriedata uden eksplicit tidsstempel. Dette hjælper med at bevare korrekt rækkefølge ved behandling af beskeder fra flere kilder eller når beskeder ankommer ude af rækkefølge.",
"kv-map-pattern-hint": "Alle inputfelter understøtter skabeloner. Brug $[messageKey] for værdi fra beskeden og ${metadataKey} for værdi fra metadata.",
"kv-map-single-pattern-hint": "Inputfeltet understøtter skabeloner. Brug $[messageKey] for værdi fra beskeden og ${metadataKey} for værdi fra metadata.",
"shared-scope": "Delt område",
"server-scope": "Serverområde",
"client-scope": "Klientområde",
"attribute-type": "Attribute",
"attribute-type-description": "Hent attributværdi fra database",
"attribute-type-result-description": "Gem som attribut i database",
"attribute-type": "Attribut",
"attribute-type-description": "Hent attributværdi fra databasen",
"attribute-type-result-description": "Gem resultatet som en entitetsattribut i databasen",
"constant-type": "Konstant",
"constant-type-description": "Definér konstant værdi",
"time-series-type": "Tidsserie",
"time-series-type-description": "Hent seneste værdi fra database",
"time-series-type-result-description": "Gem som tidsserie i database",
"time-series-type-description": "Hent seneste tidsserieværdi fra databasen",
"time-series-type-result-description": "Gem resultatet som en entitets tidsserie i databasen",
"message-body-type": "Besked",
"message-body-type-description": "Hent argumentværdi fra besked",
"message-body-type-result-description": "Tilføj resultat til besked",
"message-body-type-description": "Hent argumentværdi fra den indgående besked",
"message-body-type-result-description": "Tilføj resultat til den udgående besked",
"message-metadata-type": "Metadata",
"message-metadata-type-description": "Hent argumentværdi fra metadata",
"message-metadata-result-description": "Tilføj resultat til metadata",
"message-metadata-type-description": "Hent argumentværdi fra den indgående beskeds metadata",
"message-metadata-result-description": "Tilføj resultat til metadata for den udgående besked",
"argument-tile": "Argumenter",
"no-arguments-prompt": "Ingen argumenter konfigureret",
"result-title": "Resultat",
"functions-field-input": "Funktioner",
"no-option-found": "Ingen muligheder fundet",
"no-option-found": "Ingen valgmuligheder fundet",
"argument-source-field-input": "Kilde",
"argument-source-field-input-required": "Kilde er påkrævet.",
"argument-source-field-input-required": "Argumentkilde er påkrævet.",
"argument-key-field-input": "Nøgle",
"argument-key-field-input-required": "Nøgle er påkrævet.",
"argument-key-field-input-required": "Argumentnøgle er påkrævet.",
"constant-value-field-input": "Konstant værdi",
"constant-value-field-input-required": "Konstant værdi er påkrævet.",
"attribute-scope-field-input": "Attribut område",
"attribute-scope-field-input-required": "Område er påkrævet.",
"attribute-scope-field-input": "Attributområde",
"attribute-scope-field-input-required": "Attributområde er påkrævet.",
"default-value-field-input": "Standardværdi",
"type-field-input": "Type",
"type-field-input-required": "Type er påkrævet.",
@ -5266,21 +5291,21 @@
"clear-selected-keys": "Ryd valgte nøgler",
"geofence-configuration": "Geofence-konfiguration",
"coordinate-field-names": "Koordinatfeltnavne",
"coordinate-field-hint": "Forsøger at hente felter fra besked. Hvis ikke til stede, bruges metadata.",
"coordinate-field-hint": "Regelnoden forsøger at hente de angivne felter fra beskeden. Hvis de ikke findes, vil den forsøge at hente dem fra metadata.",
"presence-monitoring-strategy": "Strategi for tilstedeværelsesovervågning",
"presence-monitoring-strategy-on-first-message": "Ved første besked",
"presence-monitoring-strategy-on-each-message": "Ved hver besked",
"presence-monitoring-strategy-on-first-message-hint": "Rapporterer status 'Inde' eller 'Ude' ved første besked efter minimumsvarighed siden sidste status.",
"presence-monitoring-strategy-on-each-message-hint": "Rapporterer status ved hver besked efter statusændring.",
"presence-monitoring-strategy-on-first-message-hint": "Rapporterer tilstedeværelsesstatus 'Inde' eller 'Ude' ved første besked efter den konfigurerede minimumsvarighed er opnået siden sidste status 'Gået ind' eller 'Gået ud'.",
"presence-monitoring-strategy-on-each-message-hint": "Rapporterer tilstedeværelsesstatus 'Inde' eller 'Ude' ved hver besked efter statusopdatering 'Gået ind' eller 'Gået ud'.",
"fetch-credentials-to": "Hent legitimationsoplysninger til",
"add-originator-attributes-to": "Tilføj afsenders attributter til",
"originator-attributes": "Afsenders attributter",
"fetch-latest-telemetry-with-timestamp": "Hent seneste telemetry med tidsstempel",
"fetch-latest-telemetry-with-timestamp-tooltip": "Inkluderer tidsstempel i metadata, f.eks.: \"{{latestTsKeyName}}\": \"{\"ts\":1574329385897, \"value\":42}\"",
"tell-failure": "Rapportér fejl hvis attribut mangler",
"tell-failure-tooltip": "Rapporterer fejl hvis mindst én valgt nøgle mangler.",
"created-time": "Oprettelsestid",
"chip-help": "Tryk på 'Enter' for at fuldføre {{inputName}}-indtastning. \nTryk på 'Backspace' for at slette {{inputName}}. \nFlere værdier understøttes.",
"add-originator-attributes-to": "Tilføj afsenderattributter til",
"originator-attributes": "Afsenderattributter",
"fetch-latest-telemetry-with-timestamp": "Hent seneste telemetri med tidsstempel",
"fetch-latest-telemetry-with-timestamp-tooltip": "Hvis valgt, tilføjes de seneste telemetriværdier med tidsstempel til metadata, f.eks.: \"{{latestTsKeyName}}\": \"{\\\"ts\\\":1574329385897, \\\"value\\\":42}\"",
"tell-failure": "Rapportér fejl, hvis en eller flere attributter mangler",
"tell-failure-tooltip": "Hvis mindst én valgt nøgle ikke findes, vil den udgående besked rapportere \"Fejl\".",
"created-time": "Oprettelsestidspunkt",
"chip-help": "Tryk på 'Enter' for at afslutte indtastning af {{inputName}}. \nTryk på 'Backspace' for at slette {{inputName}}. \nFlere værdier understøttes.",
"detail": "detalje",
"field-name": "feltnavn",
"device-profile": "enhedsprofil",
@ -5291,19 +5316,19 @@
"first-name": "Fornavn",
"last-name": "Efternavn",
"label": "Etiket",
"originator-fields-mapping": "Afsender felttilknytning",
"add-mapped-originator-fields-to": "Tilføj mappede afsenderfelter til",
"originator-fields-mapping": "Kortlægning af afsenderfelter",
"add-mapped-originator-fields-to": "Tilføj kortlagte afsenderfelter til",
"fields": "Felter",
"skip-empty-fields": "Spring tomme felter over",
"skip-empty-fields-tooltip": "Tomme felter tilføjes ikke til uddata.",
"fetch-interval": "Hentningsinterval",
"fetch-strategy": "Hentningsstrategi",
"fetch-timeseries-from-to": "Hent tidsserier fra {{startInterval}} {{startIntervalTimeUnit}} siden til {{endInterval}} {{endIntervalTimeUnit}} siden.",
"fetch-timeseries-from-to-invalid": "Ugyldig hentning af tidsserier (\"Intervalstart\" skal være mindre end \"Intervalslut\").",
"use-metadata-dynamic-interval-tooltip": "Hvis valgt, vil rule node bruge dynamisk intervalstart og -slut baseret på mønstre i beskeden og metadata.",
"all-mode-hint": "Hvis hentningstilstand \"Alle\" er valgt, vil rule node hente telemetri fra intervallet med konfigurerbare forespørgselsparametre.",
"first-mode-hint": "Hvis hentningstilstand \"Første\" er valgt, vil rule node hente den telemetri, der ligger tættest på intervallets start.",
"last-mode-hint": "Hvis hentningstilstand \"Sidste\" er valgt, vil rule node hente den telemetri, der ligger tættest på intervallets slutning.",
"skip-empty-fields-tooltip": "Felter med tomme værdier tilføjes ikke til udgående besked eller metadata.",
"fetch-interval": "Henteinterval",
"fetch-strategy": "Hentestrategi",
"fetch-timeseries-from-to": "Hent tidsserie fra {{startInterval}} {{startIntervalTimeUnit}} siden til {{endInterval}} {{endIntervalTimeUnit}} siden.",
"fetch-timeseries-from-to-invalid": "Ugyldig tidsseriehentning (\"Interval start\" skal være mindre end \"Interval slut\").",
"use-metadata-dynamic-interval-tooltip": "Hvis valgt, bruger regelnoden dynamisk start og slut på intervallet baseret på besked- og metadatamønstre.",
"all-mode-hint": "Hvis hentemåde \"Alle\" er valgt, vil regelnoden hente telemetri fra hele intervallet baseret på konfigurerbare forespørgselsparametre.",
"first-mode-hint": "Hvis hentemåde \"Første\" er valgt, hentes den telemetri, der ligger tættest på intervalstarten.",
"last-mode-hint": "Hvis hentemåde \"Sidste\" er valgt, hentes den telemetri, der ligger tættest på intervalslutningen.",
"ascending": "Stigende",
"descending": "Faldende",
"min": "Min",
@ -5312,39 +5337,39 @@
"sum": "Sum",
"count": "Antal",
"none": "Ingen",
"last-level-relation-tooltip": "Søger kun relationer på det maksimale niveau.",
"last-level-device-relation-tooltip": "Søger kun enheder på det maksimale niveau.",
"last-level-relation-tooltip": "Hvis valgt, vil regelnoden kun søge relaterede enheder på det niveau, der er angivet som maksimal relation.",
"last-level-device-relation-tooltip": "Hvis valgt, vil regelnoden kun søge relaterede enheder på det niveau, der er angivet som maksimal relation.",
"data-to-fetch": "Data der skal hentes",
"mapping-of-customers": "Kundemapping",
"map-fields-required": "Alle felter skal udfyldes.",
"mapping-of-customers": "Kortlægning af kunde",
"map-fields-required": "Alle kortlægningsfelter er påkrævede.",
"attributes": "Attributter",
"related-device-attributes": "Attributter for relateret enhed",
"related-device-attributes": "Attributter for relaterede enheder",
"add-selected-attributes-to": "Tilføj valgte attributter til",
"device-profiles": "Enhedsprofiler",
"mapping-of-tenant": "Tenant-mapping",
"mapping-of-tenant": "Kortlægning af tenant",
"add-attribute-key": "Tilføj attributnøgle",
"message-template": "Beskedskabelon",
"message-template-required": "Beskedskabelon er påkrævet",
"use-system-slack-settings": "Brug systemets Slack-indstillinger",
"slack-api-token": "Slack API-token",
"slack-api-token-required": "Slack API-token er påkrævet",
"keys-mapping": "Nøgletilknytning",
"keys-mapping": "Nøglekortlægning",
"add-key": "Tilføj nøgle",
"recipients": "Modtagere",
"message-subject-and-content": "Besked emne og indhold",
"template-rules-hint": "Brug $[messageKey] eller ${metadataKey} til værdier.",
"originator-customer-desc": "Brug kundens oprindelige afsender.",
"originator-tenant-desc": "Brug nuværende tenant.",
"originator-related-entity-desc": "Brug relateret entitet som afsender.",
"originator-alarm-originator-desc": "Brug alarmens afsender (kun hvis besked er alarm).",
"originator-entity-by-name-pattern-desc": "Find og brug entitet ud fra navn og type.",
"email-from-template-hint": "Brug $[messageKey] eller ${metadataKey} for at trække værdier.",
"recipients-block-main-hint": "Kommasepareret liste, understøtter templatization.",
"forward-msg-default-rule-chain": "Videresend besked til standard regelkæde",
"forward-msg-default-rule-chain-tooltip": "Bruger standard regelkæde for afsender eller konfigureret kæde.",
"exclude-zero-deltas": "Udelad nul-delta værdier",
"exclude-zero-deltas-hint": "Tilføj kun nøgle hvis værdi ikke er nul.",
"exclude-zero-deltas-time-difference-hint": "Tilføj kun hvis outputværdi ≠ 0.",
"message-subject-and-content": "Beskedens emne og indhold",
"template-rules-hint": "Begge inputfelter understøtter skabeloner. Brug $[messageKey] for værdi fra besked og ${metadataKey} for værdi fra metadata.",
"originator-customer-desc": "Brug kunde tilhørende beskedens afsender som ny afsender.",
"originator-tenant-desc": "Brug nuværende tenant som ny afsender.",
"originator-related-entity-desc": "Brug relateret enhed som ny afsender. Opslag baseret på konfigureret relationstype og retning.",
"originator-alarm-originator-desc": "Brug alarmens afsender som ny afsender. Kun hvis afsenderen er en alarmenhed.",
"originator-entity-by-name-pattern-desc": "Brug enhed hentet fra databasen som ny afsender. Opslag baseret på enhedstype og angivet navnemønster.",
"email-from-template-hint": "Brug $[messageKey] for værdi fra besked og ${metadataKey} for værdi fra metadata.",
"recipients-block-main-hint": "Kommasepareret adresseliste. Alle inputfelter understøtter skabeloner. Brug $[messageKey] for værdi fra besked og ${metadataKey} for værdi fra metadata.",
"forward-msg-default-rule-chain": "Videresend besked til afsenders standardregel-kæde",
"forward-msg-default-rule-chain-tooltip": "Hvis aktiveret, videresendes besked til afsenders standardregel-kæde, eller den i konfigurationen angivne, hvis ingen standard er defineret i enhedsprofilen.",
"exclude-zero-deltas": "Ekskludér nul-deltaer fra udgående besked",
"exclude-zero-deltas-hint": "Hvis aktiveret, tilføjes nøglen \"{{outputValueKey}}\" kun, hvis værdien ikke er nul.",
"exclude-zero-deltas-time-difference-hint": "Hvis aktiveret, tilføjes \"{{outputValueKey}}\" og \"{{periodValueKey}}\" kun, hvis \"{{outputValueKey}}\" ikke er nul.",
"search-direction-from": "Fra afsender til målentitet",
"search-direction-to": "Fra målentitet til afsender",
"del-relation-direction-from": "Fra afsender",
@ -5354,33 +5379,33 @@
"function-name": "Funktionsnavn",
"function-name-required": "Funktionsnavn er påkrævet.",
"qualifier": "Kvalifikator",
"qualifier-hint": "Hvis kvalifikatoren ikke er angivet, vil standardkvalifikatoren \"$LATEST\" blive brugt.",
"qualifier-hint": "Hvis kvalifikator ikke er angivet, bruges standardkvalifikatoren \"$LATEST\".",
"aws-credentials": "AWS-legitimationsoplysninger",
"connection-timeout": "Forbindelsestimeout",
"connection-timeout-required": "Forbindelsestimeout er påkrævet.",
"connection-timeout-min": "Minimum forbindelsestimeout er 0.",
"connection-timeout-hint": "Ventetid ved etablering af forbindelse. 0 betyder uendelig og frarådes.",
"request-timeout": "Anmodningstimeout",
"request-timeout-required": "Anmodningstimeout er påkrævet",
"request-timeout-min": "Minimum anmodningstimeout er 0",
"request-timeout-hint": "Ventetid for anmodning, før timeout. 0 betyder uendelig og frarådes.",
"connection-timeout": "Forbindelsestidsgrænse",
"connection-timeout-required": "Forbindelsestidsgrænse er påkrævet.",
"connection-timeout-min": "Minimum forbindelsestidsgrænse er 0.",
"connection-timeout-hint": "Tiden i sekunder, der ventes ved etablering af forbindelse, før timeout. En værdi på 0 betyder uendelig, hvilket ikke anbefales.",
"request-timeout": "Forespørgsels-timeout",
"request-timeout-required": "Forespørgsels-timeout er påkrævet.",
"request-timeout-min": "Minimum forespørgsels-timeout er 0.",
"request-timeout-hint": "Tiden i sekunder, der ventes på at gennemføre forespørgslen, før timeout. En værdi på 0 betyder uendelig, hvilket ikke anbefales.",
"units": "Enheder",
"tell-failure-aws-lambda": "Rapportér fejl hvis AWS Lambda-funktion kaster en undtagelse",
"tell-failure-aws-lambda-hint": "Tvinger fejl hvis Lambda-funktionen fejler.",
"basic-mode": "Grundlæggende",
"tell-failure-aws-lambda": "Rapportér fejl, hvis AWS Lambda-funktion kaster en undtagelse",
"tell-failure-aws-lambda-hint": "Regelnoden tvinger fejl i beskedbehandling, hvis AWS Lambda-funktion udløser en undtagelse.",
"basic-mode": "Basis",
"advanced-mode": "Avanceret",
"save-time-series": {
"processing-settings": "Behandlingsindstillinger",
"processing-settings-hint": "Definér hvordan beskeder behandles. Grundlæggende eller avancerede strategier.",
"advanced-settings-hint": "Vær forsigtig – visse kombinationer kan føre til uventet adfærd.",
"processing-settings-hint": "Definér, hvordan indgående beskeder behandles. Basisindstillinger lader dig vælge forudkonfigurerede strategier, mens avancerede indstillinger tillader valg af individuelle strategier for hver handling.",
"advanced-settings-hint": "Vær forsigtig ved konfiguration af behandlingsstrategier. Visse kombinationer kan føre til uforudset opførsel.",
"strategy": "Strategi",
"deduplication-interval": "Deduplikeringsinterval",
"deduplication-interval-required": "Dedupliceringsinterval er påkrævet",
"deduplication-interval-min-max-range": "Skal være mellem 1 sekund og 1 dag",
"deduplication-interval": "Deduplikationsinterval",
"deduplication-interval-required": "Deduplikationsinterval er påkrævet",
"deduplication-interval-min-max-range": "Deduplikationsinterval skal være mindst 1 sekund og højst 1 dag",
"strategy-type": {
"every-message": "Ved hver besked",
"skip": "Spring over",
"deduplicate": "Dedupliker",
"deduplicate": "Deduplikér",
"web-sockets-only": "Kun WebSockets"
},
"time-series": "Tidsserier",
@ -5390,17 +5415,17 @@
},
"save-attribute": {
"processing-settings": "Behandlingsindstillinger",
"processing-settings-hint": "Definér hvordan beskeder behandles. Grundlæggende eller avancerede strategier.",
"advanced-settings-hint": "Vær forsigtig – visse kombinationer kan føre til uventet adfærd.",
"processing-settings-hint": "Definér, hvordan indgående beskeder behandles. Basisindstillinger lader dig vælge forudkonfigurerede strategier, mens avancerede indstillinger tillader valg af individuelle strategier for hver handling.",
"advanced-settings-hint": "Vær forsigtig ved konfiguration af behandlingsstrategier. Visse kombinationer kan føre til uforudset opførsel.",
"strategy": "Strategi",
"deduplication-interval": "Deduplikeringsinterval",
"deduplication-interval-required": "Deduplikeringsinterval er påkrævet",
"deduplication-interval-min-max-range": "Skal være mellem 1 sekund og 1 dag",
"deduplication-interval": "Deduplikationsinterval",
"deduplication-interval-required": "Deduplikationsinterval er påkrævet",
"deduplication-interval-min-max-range": "Deduplikationsinterval skal være mindst 1 sekund og højst 1 dag",
"scope": "Omfang",
"strategy-type": {
"every-message": "Ved hver besked",
"skip": "Spring over",
"deduplicate": "Dedupliker",
"deduplicate": "Deduplikér",
"web-sockets-only": "Kun WebSockets"
},
"attributes": "Attributter"
@ -5432,32 +5457,33 @@
"ai": {
"ai-model": "AI-model",
"model": "Model",
"ai-model-hint": "Vælg den forudkonfigurerede AI-model til at behandle forespørgsler sendt af denne rule node, eller brug \"Opret ny\" for at konfigurere en ny.",
"prompt-settings": "Promptindstillinger",
"prompt-settings-hint": "Den valgfrie systemprompt angiver AI'ens generelle rolle og begrænsninger, mens brugerprompten definerer den specifikke opgave. Begge felter understøtter også skabelonfunktionalitet.",
"ai-model-hint": "Vælg den forudkonfigurerede AI-model til at behandle anmodninger sendt af denne regelnode, eller brug \"Opret ny\" for at konfigurere en ny.",
"prompt-settings": "Prompt-indstillinger",
"prompt-settings-hint": "Den valgfrie systemprompt angiver AI'ens generelle rolle og begrænsninger, mens brugerprompten definerer den specifikke opgave, der skal udføres. Begge felter understøtter også skabeloner.",
"system-prompt": "Systemprompt",
"system-prompt-max-length": "Systemprompt må højst være 500000 tegn.",
"system-prompt-max-length": "Systemprompt må højst indeholde 500000 tegn.",
"system-prompt-blank": "Systemprompt må ikke være tom.",
"user-prompt": "Brugerprompt",
"user-prompt-required": "Brugerprompt er påkrævet.",
"user-prompt-max-length": "Brugerprompt må højst være 500000 tegn.",
"user-prompt-max-length": "Brugerprompt må højst indeholde 500000 tegn.",
"user-prompt-blank": "Brugerprompt må ikke være tom.",
"response-format": "Svarformat",
"response-format": "Svartype",
"response-text": "Tekst",
"response-json": "JSON",
"response-json-schema": "JSON-skema",
"response-format-hint-TEXT": "Tillader modellen at generere vilkårlig tekst, som måske eller måske ikke er gyldig JSON. Hvis output ikke er gyldig JSON, bliver det automatisk pakket ind i en JSON med nøglen \"response\".",
"response-format-hint-JSON": "Modellen skal generere et svar, der er gyldig JSON. Hvis output ikke er gyldig JSON, bliver det automatisk pakket ind i en JSON med nøglen \"response\".",
"response-format-hint-JSON_SCHEMA": "Modellen skal generere en JSON, der matcher strukturen og datatyperne defineret i det angivne skema. Hvis output ikke er gyldig JSON, bliver det automatisk pakket ind i en JSON med nøglen \"response\".",
"response-json-schema-hint": "Selvom enhver gyldig JSON-skema kan indtastes, understøtter denne rule node kun et begrænset sæt funktioner. Se dokumentationen for detaljer.",
"response-format-hint-TEXT": "Tillader modellen at generere vilkårlig tekst, som muligvis ikke er et gyldigt JSON-objekt. Hvis output ikke er gyldigt JSON, vil det automatisk blive pakket ind i et JSON-objekt under nøglen \"response\".",
"response-format-hint-JSON": "Modellen skal generere et svar, der er gyldigt JSON. Hvis output ikke er gyldigt JSON, vil det automatisk blive pakket ind i et JSON-objekt under nøglen \"response\".",
"response-format-hint-JSON_SCHEMA": "Modellen skal generere JSON, der matcher den specifikke struktur og datatyper defineret i det angivne skema. Hvis output ikke er gyldigt JSON, vil det automatisk blive pakket ind i et JSON-objekt under nøglen \"response\".",
"response-json-schema-hint": "Selvom enhver gyldig JSON-skema kan indtastes, understøtter denne regelnode kun et begrænset sæt af dets funktioner. Se node-dokumentationen for detaljer.",
"response-json-schema-required": "JSON-skema er påkrævet",
"advanced-settings": "Avancerede indstillinger",
"timeout": "Timeout",
"timeout-hint": "Maksimalt tidsrum at vente på svar \nfra AI-modellen, før forespørgslen afsluttes.",
"timeout-hint": "Maksimal ventetid på svar fra AI-modellen, før anmodningen afbrydes.",
"timeout-required": "Timeout er påkrævet",
"timeout-validation": "Skal være mellem 1 sekund og 10 minutter.",
"force-acknowledgement": "Tving kvittering",
"force-acknowledgement-hint": "Hvis aktiveret, kvitteres der straks for den indgående besked. Modellens svar bliver derefter sat i kø som en separat, ny besked."
"force-acknowledgement-hint": "Hvis aktiveret, kvitteres den indgående besked straks. Modellens svar sættes derefter i kø som en separat, ny besked.",
"ai-resources": "AI-ressourcer"
}
},
"timezone": {
@ -5757,9 +5783,9 @@
"rule-engine-exceptions-ttl-days": "Rule Engine-undtagelser TTL dage",
"rule-engine-exceptions-ttl-days-required": "Rule Engine-undtagelser TTL dage er påkrævet",
"rule-engine-exceptions-ttl-days-range": "Rule Engine-undtagelser TTL dage må ikke være negativt",
"max-rule-node-executions-per-message": "Maksimalt antal regelnode-udførelser pr. besked",
"max-rule-node-executions-per-message-required": "Maksimalt antal regelnode-udførelser pr. besked er påkrævet.",
"max-rule-node-executions-per-message-range": "Maksimalt antal regelnode-udførelser pr. besked må ikke være negativt",
"max-rule-node-executions-per-message": "Maks. antal regelnodekørsler pr. besked",
"max-rule-node-executions-per-message-required": "Maks. antal regelnodekørsler pr. besked er påkrævet.",
"max-rule-node-executions-per-message-range": "Maks. antal regelnodekørsler pr. besked må ikke være negativ",
"max-emails": "Maksimalt antal sendte e-mails",
"max-emails-required": "Maksimalt antal sendte e-mails er påkrævet.",
"max-emails-range": "Maksimalt antal sendte e-mails må ikke være negativt",
@ -5987,9 +6013,10 @@
"label": "Etiket",
"value": "Værdi",
"date": "Dato",
"show-date-time-interval": "Vis datotidsinterval",
"show-date-time-interval-hint": "Vis datotidsinterval i henhold til dataaggregering.",
"show-date-time-interval": "Vis dato-/tidsinterval",
"show-date-time-interval-hint": "Vis dato-/tidsinterval i henhold til dataaggregering.",
"hide-zero-tooltip-values": "Skjul nulværdier",
"show-stack-total": "Vis totalværdi i stablet tilstand",
"background-color": "Baggrundsfarve",
"background-blur": "Baggrundssløring"
},
@ -6702,15 +6729,15 @@
"no-widgets-text": "Ingen widgets fundet",
"management": "Widgetadministration",
"editor": "Widget-editor",
"confirm-to-exit-editor-html": "Du har ikke gemt dine widgetindstillinger.<br>Er du sikker på, at du vil forlade denne side?",
"widget-type-not-found": "Problem med at indlæse widgetkonfiguration.<br>Den tilknyttede widgettype er sandsynligvis blevet fjernet.",
"confirm-to-exit-editor-html": "Du har ikke gemte widget-indstillinger.<br>Er du sikker på, at du vil forlade denne side?",
"widget-type-not-found": "Problem med indlæsning af widget-konfiguration.<br>Den tilknyttede widget-type er\n sandsynligvis blevet fjernet.",
"widget-type-load-error": "Widget blev ikke indlæst på grund af følgende fejl:",
"remove": "Fjern widget",
"delete": "Slet widget",
"edit": "Rediger widget",
"remove-widget-title": "Er du sikker på, at du vil fjerne widgetten '{{widgetTitle}}'?",
"remove-widget-text": "Efter bekræftelse vil widgetten og alle tilknyttede data ikke kunne gendannes.",
"replace-reference-with-widget-copy": "Erstat reference med kopi af widget",
"remove-widget-text": "Efter bekræftelse bliver widgetten og alle relaterede data uigenkaldeligt slettet.",
"replace-reference-with-widget-copy": "Erstat reference med en kopi af widget",
"timeseries": "Tidsserier",
"search-data": "Søg data",
"no-data-found": "Ingen data fundet",
@ -7550,8 +7577,8 @@
"October": "Oktober",
"November": "November",
"December": "December",
"Custom Date Range": "Brugerdefineret datoområde",
"Date Range Template": "Skabelon for datoområde",
"Custom Date Range": "Brugerdefineret datointerval",
"Date Range Template": "Datointervalskabelon",
"Today": "I dag",
"Yesterday": "I går",
"This Week": "Denne uge",
@ -7572,7 +7599,7 @@
"Custom interval": "Brugerdefineret interval",
"Interval": "Interval",
"Step size": "Trinstørrelse",
"Ok": "OK"
"Ok": "Ok"
}
},
"doughnut": {
@ -7784,11 +7811,11 @@
"gpio-switches": "GPIO-kontakter",
"no-gpio-switches": "Ingen GPIO-kontakter konfigureret",
"add-gpio-switch": "Tilføj GPIO-kontakt",
"gpio-status-request": "GPIO-statusanmodning",
"gpio-status-request": "GPIO-statusforespørgsel",
"method-name": "Metodenavn",
"method-body": "Metodeindhold",
"gpio-status-change-request": "Anmodning om ændring af GPIO-status",
"parse-gpio-status-function": "Funktion til at fortolke GPIO-status",
"parse-gpio-status-function": "Parse-funktion for GPIO-status",
"gpio-leds": "GPIO-LED'er",
"no-gpio-leds": "Ingen GPIO-LED'er konfigureret",
"add-gpio-led": "Tilføj GPIO-LED"
@ -9522,4 +9549,4 @@
"language": {
"language": "Sprog"
}
}
}

365
ui-ngx/src/assets/locale/locale.constant-de_DE.json

@ -163,39 +163,39 @@
"aws-secret-access-key-required": "AWS Secret Access Key ist erforderlich",
"aws-region": "AWS-Region",
"aws-region-required": "AWS-Region ist erforderlich",
"number-from": "Absendernummer",
"number-from-required": "Absendernummer ist erforderlich.",
"number-to": "Empfängernummer",
"number-to-required": "Empfängernummer ist erforderlich.",
"phone-number-hint": "Telefonnummer im E.164-Format, z. B. +19995550123",
"phone-number-hint-twilio": "Telefonnummer im E.164-Format/SID der Telefonnummer/Messaging Service SID, z. B. +19995550123/PNXXX/MGXXX",
"phone-number-pattern": "Ungültige Telefonnummer. Sollte im E.164-Format sein, z. B. +19995550123.",
"phone-number-pattern-twilio": "Ungültige Telefonnummer. Sollte im E.164-Format/SID der Telefonnummer/Messaging Service SID sein.",
"number-from": "Telefonnummer Von",
"number-from-required": "Telefonnummer Von ist erforderlich.",
"number-to": "Telefonnummer An",
"number-to-required": "Telefonnummer An ist erforderlich.",
"phone-number-hint": "Telefonnummer im E.164-Format, z. B. +19995550123",
"phone-number-hint-twilio": "Telefonnummer im E.164-Format/SID der Telefonnummer/SID des Messaging-Dienstes, z. B. +19995550123/PNXXX/MGXXX",
"phone-number-pattern": "Ungültige Telefonnummer. Sollte im E.164-Format vorliegen, z. B. +19995550123.",
"phone-number-pattern-twilio": "Ungültige Telefonnummer. Sollte im E.164-Format/SID der Telefonnummer/SID des Messaging-Dienstes vorliegen, z. B. +19995550123/PNXXX/MGXXX.",
"sms-message": "SMS-Nachricht",
"sms-message-required": "SMS-Nachricht ist erforderlich.",
"sms-message-max-length": "SMS-Nachricht darf nicht länger als 1600 Zeichen sein",
"twilio-account-sid": "Twilio Account SID",
"twilio-account-sid-required": "Twilio Account SID ist erforderlich",
"twilio-account-token": "Twilio Account Token",
"twilio-account-token-required": "Twilio Account Token ist erforderlich",
"twilio-account-sid": "Twilio-Konto SID",
"twilio-account-sid-required": "Twilio-Konto SID ist erforderlich",
"twilio-account-token": "Twilio-Konto-Token",
"twilio-account-token-required": "Twilio-Konto-Token ist erforderlich",
"send-test-sms": "Test-SMS senden",
"test-sms-sent": "Test-SMS wurde erfolgreich gesendet!",
"security-settings": "Sicherheitseinstellungen",
"password-policy": "Passwortrichtlinie",
"minimum-password-length": "Minimale Passwortlänge",
"minimum-password-length-required": "Minimale Passwortlänge ist erforderlich",
"minimum-password-length-range": "Minimale Passwortlänge sollte zwischen 6 und 50 Zeichen liegen",
"minimum-password-length-range": "Die minimale Passwortlänge muss zwischen 6 und 50 liegen",
"maximum-password-length": "Maximale Passwortlänge",
"maximum-password-length-min": "Maximale Passwortlänge sollte mindestens 6 betragen",
"maximum-password-length-less-min": "Maximale Passwortlänge muss größer als die minimale Länge sein",
"minimum-uppercase-letters": "Minimale Anzahl an Großbuchstaben",
"minimum-uppercase-letters-range": "Minimale Anzahl an Großbuchstaben darf nicht negativ sein",
"minimum-lowercase-letters": "Minimale Anzahl an Kleinbuchstaben",
"minimum-lowercase-letters-range": "Minimale Anzahl an Kleinbuchstaben darf nicht negativ sein",
"minimum-digits": "Minimale Anzahl an Ziffern",
"minimum-digits-range": "Minimale Anzahl an Ziffern darf nicht negativ sein",
"minimum-special-characters": "Minimale Anzahl an Sonderzeichen",
"minimum-special-characters-range": "Minimale Anzahl an Sonderzeichen darf nicht negativ sein",
"maximum-password-length-min": "Die maximale Passwortlänge muss mindestens 6 betragen",
"maximum-password-length-less-min": "Die maximale Passwortlänge muss größer als die minimale sein",
"minimum-uppercase-letters": "Mindestanzahl an Großbuchstaben",
"minimum-uppercase-letters-range": "Die Mindestanzahl an Großbuchstaben darf nicht negativ sein",
"minimum-lowercase-letters": "Mindestanzahl an Kleinbuchstaben",
"minimum-lowercase-letters-range": "Die Mindestanzahl an Kleinbuchstaben darf nicht negativ sein",
"minimum-digits": "Mindestanzahl an Ziffern",
"minimum-digits-range": "Die Mindestanzahl an Ziffern darf nicht negativ sein",
"minimum-special-characters": "Mindestanzahl an Sonderzeichen",
"minimum-special-characters-range": "Die Mindestanzahl an Sonderzeichen darf nicht negativ sein",
"password-expiration-period-days": "Passwort-Ablaufzeitraum in Tagen",
"password-expiration-period-days-range": "Passwort-Ablaufzeitraum darf nicht negativ sein",
"password-reuse-frequency-days": "Häufigkeit der Passwortwiederverwendung in Tagen",
@ -676,8 +676,8 @@
"alias": {
"add": "Alias hinzufügen",
"edit": "Alias bearbeiten",
"name": "Aliasname",
"name-required": "Aliasname ist erforderlich",
"name": "Alias-Name",
"name-required": "Alias-Name ist erforderlich",
"duplicate-alias": "Ein Alias mit demselben Namen existiert bereits.",
"filter-type-single-entity": "Einzelne Entität",
"filter-type-entity-list": "Entitätsliste",
@ -864,15 +864,15 @@
"api-features": "API-Funktionen",
"api-usage": "API-Nutzung",
"alarm": "Alarm",
"alarms-created": "Erstellte Alarme",
"alarms-created": "Alarme erstellt",
"queue-stats": "Warteschlangenstatistiken",
"processing-failures-and-timeouts": "Verarbeitungsfehler und Zeitüberschreitungen",
"exceptions": "Ausnahmen",
"alarms-created-daily-activity": "Tägliche Aktivität der Alarmerstellung",
"alarms-created-hourly-activity": "Stündliche Aktivität der Alarmerstellung",
"alarms-created-monthly-activity": "Monatliche Aktivität der Alarmerstellung",
"alarms-created-daily-activity": "Tägliche Aktivität der Alarm-Erstellung",
"alarms-created-hourly-activity": "Stündliche Aktivität der Alarm-Erstellung",
"alarms-created-monthly-activity": "Monatliche Aktivität der Alarm-Erstellung",
"data-points": "Datenpunkte",
"data-points-storage-days": "Speichertage der Datenpunkte",
"data-points-storage-days": "Speicherzeitraum der Datenpunkte (in Tagen)",
"device-api": "Geräte-API",
"email": "E-Mail",
"email-messages": "E-Mail-Nachrichten",
@ -1111,13 +1111,15 @@
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
"github-models": "GitHub-Modelle"
"github-models": "GitHub-Modelle",
"ollama": "Ollama"
},
"name-required": "Name ist erforderlich.",
"name-max-length": "Der Name darf höchstens 255 Zeichen lang sein.",
"provider": "Anbieter",
"api-key": "API-Schlüssel",
"api-key-required": "API-Schlüssel ist erforderlich.",
"api-key-open-ai-required": "API-Schlüssel ist erforderlich, wenn die offizielle OpenAI-API verwendet wird.",
"project-id": "Projekt-ID",
"project-id-required": "Projekt-ID ist erforderlich.",
"location": "Standort",
@ -1147,24 +1149,41 @@
"top-p-hint": "Erstellt einen Pool der wahrscheinlichsten Tokens, aus denen das Modell auswählt. Höhere Werte erweitern den Pool, niedrigere verkleinern ihn.",
"top-p-min-max": "Muss größer als 0 und maximal 1 sein.",
"top-k": "Top K",
"top-k-hint": "Begrenzt die Auswahl des Modells auf die „K“ wahrscheinlichsten Tokens.",
"top-k-hint": "Begrenzt die Auswahl des Modells auf die \"K\" wahrscheinlichsten Tokens.",
"top-k-min": "Muss 0 oder größer sein.",
"presence-penalty": "Strafe für Anwesenheit",
"presence-penalty": "Präsenzstrafe",
"presence-penalty-hint": "Wendet eine feste Strafe auf die Wahrscheinlichkeit eines Tokens an, wenn es bereits im Text erschienen ist.",
"frequency-penalty": "Strafe für Häufigkeit",
"frequency-penalty-hint": "Verringert die Wahrscheinlichkeit eines Tokens basierend auf seiner Häufigkeit im Text.",
"frequency-penalty": "Frequenzstrafe",
"frequency-penalty-hint": "Reduziert die Wahrscheinlichkeit eines Tokens abhängig von seiner Häufigkeit im Text.",
"max-output-tokens": "Maximale Ausgabetokens",
"max-output-tokens-min": "Muss größer als 0 sein.",
"max-output-tokens-hint": "Legt die maximale Anzahl an Tokens fest, die das Modell in einer Antwort generieren kann.",
"max-output-tokens-hint": "Legt die maximale Anzahl an Tokens fest, die das \nModell in einer Antwort generieren kann.",
"context-length": "Kontextlänge",
"context-length-hint": "Definiert die Größe des Kontextfensters in Tokens. Dieser Wert legt das Gesamtspeicherlimit für das Modell fest – einschließlich Benutzereingabe und generierter Antwort.",
"endpoint": "Endpunkt",
"endpoint-required": "Endpunkt ist erforderlich.",
"baseurl": "Basis-URL",
"baseurl-required": "Basis-URL ist erforderlich.",
"service-version": "Service-Version",
"check-connectivity": "Konnektivität prüfen",
"check-connectivity-success": "Testanfrage war erfolgreich",
"check-connectivity-failed": "Testanfrage fehlgeschlagen",
"no-model-matching": "Keine mit '{{entity}}' übereinstimmenden Modelle gefunden.",
"no-model-matching": "Keine Modelle gefunden, die mit '{{entity}}' übereinstimmen.",
"model-required": "Modell ist erforderlich.",
"no-model-text": "Keine Modelle gefunden."
"no-model-text": "Keine Modelle gefunden.",
"authentication": "Authentifizierung",
"authentication-basic-hint": "Verwendet die Standard-HTTP-Basis-Authentifizierung. Benutzername und Passwort werden kombiniert, Base64-codiert und im \"Authorization\"-Header an den Ollama-Server gesendet.",
"authentication-token-hint": "Verwendet die Bearer-Token-Authentifizierung. Das bereitgestellte Token wird direkt im \"Authorization\"-Header an den Ollama-Server gesendet.",
"authentication-type": {
"none": "Keine",
"basic": "Basis",
"token": "Token"
},
"username": "Benutzername",
"username-required": "Benutzername ist erforderlich.",
"password": "Passwort",
"password-required": "Passwort ist erforderlich.",
"token": "Token",
"token-required": "Token ist erforderlich."
},
"confirm-on-exit": {
"message": "Sie haben ungespeicherte Änderungen. Möchten Sie diese Seite wirklich verlassen?",
@ -1228,7 +1247,7 @@
"required-fields": "Erforderliche Felder fehlen"
},
"content-type": {
"json": "Json",
"json": "JSON",
"text": "Text",
"binary": "Binär (Base64)"
},
@ -1261,11 +1280,11 @@
"add-customer-text": "Neuen Kunden hinzufügen",
"no-customers-text": "Keine Kunden gefunden",
"customer-details": "Kundendetails",
"delete-customer-title": "Möchten Sie den Kunden '{{customerTitle}}' wirklich löschen?",
"delete-customer-text": "Achtung, nach der Bestätigung werden der Kunde und alle zugehörigen Daten unwiederbringlich gelöscht.",
"delete-customers-title": "Möchten Sie { count, plural, =1 {1 Kunden} other {# Kunden} } wirklich löschen?",
"delete-customer-title": "Sind Sie sicher, dass Sie den Kunden '{{customerTitle}}' löschen möchten?",
"delete-customer-text": "Achtung: Nach der Bestätigung werden der Kunde und alle zugehörigen Daten unwiderruflich gelöscht.",
"delete-customers-title": "Sind Sie sicher, dass Sie { count, plural, =1 {1 Kunden} other {# Kunden} } löschen möchten?",
"delete-customers-action-title": "{ count, plural, =1 {1 Kunden löschen} other {# Kunden löschen} }",
"delete-customers-text": "Achtung, nach der Bestätigung werden alle ausgewählten Kunden und alle zugehörigen Daten gelöscht.",
"delete-customers-text": "Achtung: Nach der Bestätigung werden alle ausgewählten Kunden und deren zugehörige Daten unwiderruflich gelöscht.",
"manage-users": "Benutzer verwalten",
"manage-assets": "Assets verwalten",
"manage-devices": "Geräte verwalten",
@ -1578,13 +1597,13 @@
"time-prev-description": "Zeitstempel des vorherigen Werts;",
"prev-orig-value-description": "ursprünglicher vorheriger Wert;",
"aggregation": "Aggregation",
"aggregation-type-hint-common": "Aus Leistungsgründen ist die Berechnung aggregierter Werte nur für feste Zeitintervalle wie 'heutiger Tag', 'aktueller Monat' usw. verfügbar, nicht jedoch für gleitende Zeitfenster wie 'letzte 30 Minuten'.",
"aggregation-type-hint-common": "Aus Leistungsgründen ist die Aggregationsberechnung nur für feste Zeitintervalle wie \"aktueller Tag\", \"aktueller Monat\" usw. verfügbar und nicht für gleitende Zeitfenster wie \"letzte 30 Minuten\" oder \"letzte 24 Stunden\".",
"aggregation-type-none-hint": "Letzten Wert übernehmen.",
"aggregation-type-min-hint": "Mindestwert innerhalb des ausgewählten Zeitfensters finden.",
"aggregation-type-max-hint": "Höchstwert innerhalb des ausgewählten Zeitfensters finden.",
"aggregation-type-avg-hint": "Durchschnittswert innerhalb des ausgewählten Zeitfensters berechnen.",
"aggregation-type-sum-hint": "Alle Werte der Datenpunkte im Zeitfenster summieren.",
"aggregation-type-count-hint": "Gesamtanzahl der Datenpunkte im Zeitfenster.",
"aggregation-type-min-hint": "Minimalwert unter den Datenpunkten innerhalb eines ausgewählten Zeitfensters ermitteln.",
"aggregation-type-max-hint": "Maximalwert unter den Datenpunkten innerhalb eines ausgewählten Zeitfensters ermitteln.",
"aggregation-type-avg-hint": "Durchschnittswert der Datenpunkte innerhalb eines ausgewählten Zeitfensters berechnen.",
"aggregation-type-sum-hint": "Summe aller Werte der Datenpunkte innerhalb eines ausgewählten Zeitfensters berechnen.",
"aggregation-type-count-hint": "Gesamtanzahl der Datenpunkte innerhalb eines ausgewählten Zeitfensters.",
"delta-calculation": "Delta-Berechnung",
"enable-delta-calculation": "Delta-Berechnung aktivieren",
"enable-delta-calculation-hint": "Wenn aktiviert, wird der Wert basierend auf aggregierten Werten für ein ausgewähltes Zeitfenster und einen Vergleichszeitraum berechnet. Delta-Berechnung ist nur für historische Zeitfenster verfügbar, nicht für Echtzeitwerte.",
@ -1991,19 +2010,19 @@
"create-new-device-profile": "Neues erstellen!",
"mqtt-device-topic-filters": "MQTT-Gerät-Topic-Filter",
"mqtt-device-topic-filters-unique": "MQTT-Gerät-Topic-Filter müssen eindeutig sein.",
"mqtt-device-topic-filters-spark-plug": "MQTT Sparkplug B Edge-of-Network (EoN) Node.",
"mqtt-device-topic-filters-spark-plug-hint": "Erlaubt Verbindungen von EoN-Nodes mit Sparkplug B Nutzlast und Topic-Format.",
"mqtt-device-topic-filters-spark-plug-attribute-metric-names": "SparkPlug-Metriken zur Speicherung als Attribute.",
"mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "Namen der SparkPlug-Metriken, die als Geräteattribute gespeichert werden. Alle anderen Metriken werden als Telemetrie gespeichert.",
"mqtt-device-payload-type": "MQTT-Geräte-Nutzlast",
"mqtt-device-topic-filters-spark-plug": "MQTT Sparkplug B Edge-of-Network (EoN)-Knoten.",
"mqtt-device-topic-filters-spark-plug-hint": "Erlaubt Verbindungen von EoN-Knoten mit Sparkplug B Payload- und Topic-Format.",
"mqtt-device-topic-filters-spark-plug-attribute-metric-names": "SparkPlug-Metriken, die als Attribute gespeichert werden.",
"mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "Namen der SparkPlug-Metriken, die als Geräteattribute gespeichert werden. Alle anderen Metriken werden als Gerätetelemetrie gespeichert.",
"mqtt-device-payload-type": "MQTT-Geräte-Payload",
"mqtt-device-payload-type-json": "JSON",
"mqtt-device-payload-type-proto": "Protobuf",
"mqtt-enable-compatibility-with-json-payload-format": "Kompatibilität mit anderen Nutzlast-Formaten aktivieren.",
"mqtt-enable-compatibility-with-json-payload-format-hint": "Wenn aktiviert, wird standardmäßig das Protobuf-Format verwendet. Bei Parsing-Fehlern wird JSON genutzt. Nützlich für die Abwärtskompatibilität bei Firmware-Updates. Dieser Modus kann die Leistung leicht beeinträchtigen und sollte nach Abschluss der Updates deaktiviert werden.",
"mqtt-use-json-format-for-default-downlink-topics": "JSON-Format für Standard-Downlink-Themen verwenden",
"mqtt-use-json-format-for-default-downlink-topics-hint": "Wenn aktiviert, wird das JSON-Format für die Standard-Downlink-Themen verwendet, z.B.: <b>v1/devices/me/attributes/response/$request_id</b>. Neue v2-Themen sind davon nicht betroffen.",
"mqtt-send-ack-on-validation-exception": "PUBACK bei Validierungsfehler senden",
"mqtt-send-ack-on-validation-exception-hint": "Standardmäßig wird die Sitzung bei einem Validierungsfehler geschlossen. Wenn aktiviert, wird stattdessen eine Bestätigung gesendet.",
"mqtt-enable-compatibility-with-json-payload-format": "Kompatibilität mit anderen Payload-Formaten aktivieren.",
"mqtt-enable-compatibility-with-json-payload-format-hint": "Wenn aktiviert, verwendet die Plattform standardmäßig das Protobuf-Payload-Format. Falls das Parsing fehlschlägt, versucht die Plattform, das JSON-Format zu verwenden. Nützlich für Abwärtskompatibilität bei Firmware-Updates. Beispielsweise nutzt die ursprüngliche Firmware-Version JSON, während die neue Version Protobuf verwendet. Während des Firmware-Update-Prozesses für die gesamte Geräteflotte ist es erforderlich, beide Formate gleichzeitig zu unterstützen. Der Kompatibilitätsmodus führt zu einer geringen Performanceeinbuße und sollte deaktiviert werden, sobald alle Geräte aktualisiert sind.",
"mqtt-use-json-format-for-default-downlink-topics": "JSON-Format für Standard-Downlink-Topics verwenden",
"mqtt-use-json-format-for-default-downlink-topics-hint": "Wenn aktiviert, verwendet die Plattform das JSON-Payload-Format zum Übertragen von Attributen und RPC über folgende Topics: <b>v1/devices/me/attributes/response/$request_id</b>, <b>v1/devices/me/attributes</b>, <b>v1/devices/me/rpc/request/$request_id</b>, <b>v1/devices/me/rpc/response/$request_id</b>. Diese Einstellung beeinflusst nicht die Verwendung neuer (v2) Topics: <b>v2/a/res/$request_id</b>, <b>v2/a</b>, <b>v2/r/req/$request_id</b>, <b>v2/r/res/$request_id</b>. Dabei ist <b>$request_id</b> ein ganzzahliger Anfrage-Identifier.",
"mqtt-send-ack-on-validation-exception": "PUBACK bei PUBLISH-Validierungsfehler senden",
"mqtt-send-ack-on-validation-exception-hint": "Standardmäßig beendet die Plattform die MQTT-Sitzung bei einem Validierungsfehler. Wenn aktiviert, sendet die Plattform stattdessen eine Bestätigung (PUBACK), anstatt die Sitzung zu schließen.",
"mqtt-protocol-version": "Protokollversion",
"snmp-add-mapping": "SNMP-Zuordnung hinzufügen",
"snmp-mapping-not-configured": "Keine Zuordnung für OID zu Zeitreihen/Telemetrie konfiguriert",
@ -2038,11 +2057,11 @@
"rpc-request-proto-schema": "RPC-Anfrage-Protoschema",
"rpc-request-proto-schema-required": "RPC-Anfrage-Protoschema ist erforderlich.",
"rpc-request-proto-schema-hint": "RPC-Anforderungsnachrichten müssen die Felder 'string method = 1;', 'int32 requestId = 2;' und 'params = 3' enthalten.",
"not-valid-pattern-topic-filter": "Ungültiges Muster im Topic-Filter",
"not-valid-single-character": "Ungültige Verwendung eines Single-Level-Wildcards",
"not-valid-multi-character": "Ungültige Verwendung eines Multi-Level-Wildcards",
"single-level-wildcards-hint": "<code>[+]</code> kann jedes Topic-Level ersetzen. Beispiel: <b>v1/devices/+/telemetry</b>.",
"multi-level-wildcards-hint": "<code>[#]</code> ersetzt mehrere Ebenen und muss am Ende des Topics stehen. Beispiel: <b>#</b> oder <b>v1/devices/me/#</b>.",
"not-valid-pattern-topic-filter": "Ungültiges Topic-Filter-Muster",
"not-valid-single-character": "Ungültige Verwendung eines einstelligen Platzhalters",
"not-valid-multi-character": "Ungültige Verwendung eines mehrstufigen Platzhalters",
"single-level-wildcards-hint": "<code>[+]</code> eignet sich für jede Ebene eines Topic-Filters. Beispiel: <b>v1/devices/+/telemetry</b> oder <b>+/devices/+/attributes</b>.",
"multi-level-wildcards-hint": "<code>[#]</code> kann das gesamte Topic-Filter ersetzen und muss das letzte Symbol im Topic sein. Beispiel: <b>#</b> oder <b>v1/devices/me/#</b>.",
"alarm-rules": "Alarmregeln",
"alarm-rules-with-count": "Alarmregeln ({{count}})",
"no-alarm-rules": "Keine Alarmregeln konfiguriert",
@ -2050,8 +2069,8 @@
"edit-alarm-rule": "Alarmregel bearbeiten",
"alarm-type": "Alarmtyp",
"alarm-type-required": "Alarmtyp ist erforderlich.",
"alarm-type-unique": "Alarmtyp muss innerhalb der Alarmregeln des Geräteprofils eindeutig sein.",
"alarm-type-max-length": "Alarmtyp sollte weniger als 256 Zeichen enthalten",
"alarm-type-unique": "Der Alarmtyp muss innerhalb der Geräteprofil-Alarmregeln eindeutig sein.",
"alarm-type-max-length": "Alarmtyp darf maximal 256 Zeichen lang sein",
"create-alarm-pattern": "<b>{{alarmType}}</b> Alarm erstellen",
"create-alarm-rules": "Alarmregeln erstellen",
"no-create-alarm-rules": "Keine Erstellungsbedingungen konfiguriert",
@ -2072,16 +2091,16 @@
"advanced-settings": "Erweiterte Einstellungen",
"alarm-rule-additional-info": "Zusätzliche Informationen",
"edit-alarm-rule-additional-info": "Zusätzliche Informationen bearbeiten",
"alarm-rule-additional-info-placeholder": "Bitte Kommentare und Anpassungen angeben, die unter 'Zusätzliche Informationen' bei Alarmdetails angezeigt werden.",
"alarm-rule-additional-info-hint": "Hinweis: Verwenden Sie <code><span style=\"color: #000;\">$&#123;</span>keyName<span style=\"color: #000;\">&#125;</span></code> zur Ersetzung von Attribut- oder Telemetriedaten.",
"alarm-rule-additional-info-placeholder": "Bitte geben Sie hier Ihre Kommentare und Anpassungen ein, um sie unter 'Zusätzliche Informationen' in den Alarmdetails anzuzeigen.",
"alarm-rule-additional-info-hint": "Hinweis: Verwenden Sie <code><span style=\"color: #000;\">$&#123;</span>keyName<span style=\"color: #000;\">&#125;</span></code>, um Werte von Attribut- oder Telemetrie-Schlüsseln zu ersetzen, die in der Alarmregel-Bedingung verwendet werden.",
"alarm-rule-mobile-dashboard": "Mobiles Dashboard",
"alarm-rule-mobile-dashboard-hint": "Wird von der mobilen App als Alarminformationen-Dashboard genutzt",
"alarm-rule-mobile-dashboard-hint": "Wird von der mobilen Anwendung als Dashboard für Alarmdetails verwendet",
"alarm-rule-no-mobile-dashboard": "Kein Dashboard ausgewählt",
"propagate-alarm": "Alarm auf verbundene Entitäten propagieren",
"propagate-alarm": "Alarm an zugehörige Entitäten weiterleiten",
"alarm-rule-relation-types-list": "Beziehungstypen",
"alarm-rule-relation-types-list-hint": "Definiert Beziehungstypen zur Filterung der verbundenen Entitäten. Wenn nicht gesetzt, wird der Alarm an alle verbundenen Entitäten propagiert.",
"propagate-alarm-to-owner": "Alarm an Eigentümer der Entität (Kunde oder Mandant) propagieren",
"propagate-alarm-to-tenant": "Alarm an Mandant propagieren",
"alarm-rule-relation-types-list-hint": "Definiert Beziehungstypen zur Filterung der zugehörigen Entitäten. Wenn nicht gesetzt, wird der Alarm an alle zugehörigen Entitäten weitergeleitet.",
"propagate-alarm-to-owner": "Alarm an Eigentümer der Entität weiterleiten (Kunde oder Tenant)",
"propagate-alarm-to-tenant": "Alarm an den Tenant weiterleiten",
"alarm-rule-condition": "Alarmregelbedingung",
"enter-alarm-rule-condition-prompt": "Bitte fügen Sie eine Alarmregelbedingung hinzu",
"edit-alarm-rule-condition": "Alarmregelbedingung bearbeiten",
@ -2237,28 +2256,28 @@
"tqs": "TQS: Sowohl TCP- als auch SMS-Verbindungen aktiv; TCP im Queue-Modus, SMS im Standardmodus (nicht unterstützt seit LWM2M 1.1)",
"sq": "SQ: SMS-Verbindung im Queue-Modus (nicht unterstützt seit LWM2M 1.1)"
},
"binding-tooltip": "Liste der unterstützten Bindungsmodi im \"binding\"-Ressourcenelement des LwM2M-Serverobjekts - /1/x/7.\nMehrere Übertragungsmodi sind möglich, aber nur einer wird während einer Transport-Session genutzt.",
"binding-tooltip": "Dies ist die Liste im \"binding\"-Ressourcenelement des LwM2M-Serverobjekts – /1/x/7.\nGibt die unterstützten Binding-Modi im LwM2M-Client an.\nDieser Wert SOLLTE mit dem Wert in der Ressource „Supported Binding and Modes“ im Geräteobjekt (/3/0/16) übereinstimmen.\nObwohl mehrere Transports unterstützt werden, kann während einer gesamten Transportsitzung nur ein Transport-Binding verwendet werden.\nBeispiel: Wenn sowohl UDP als auch SMS unterstützt werden, können der LwM2M-Client und der LwM2M-Server während der gesamten Transportsitzung entweder über UDP oder SMS kommunizieren.",
"bootstrap-server": "Bootstrap-Server",
"lwm2m-server": "LwM2M-Server",
"include-bootstrap-server": "Bootstrap-Server-Updates einbeziehen",
"bootstrap-update-title": "Bootstrap-Server ist bereits konfiguriert. Möchten Sie die Updates wirklich ausschließen?",
"bootstrap-update-text": "Vorsicht: Nach der Bestätigung gehen die Konfigurationsdaten des Bootstrap-Servers verloren.",
"bootstrap-update-text": "Achtung: Nach der Bestätigung werden die Konfigurationsdaten des Bootstrap-Servers unwiderruflich gelöscht.",
"server-host": "Host",
"server-host-required": "Host ist erforderlich.",
"server-port": "Port",
"server-port-required": "Port ist erforderlich.",
"server-port-pattern": "Port muss eine positive Ganzzahl sein.",
"server-port-range": "Port sollte im Bereich von 1 bis 65535 liegen.",
"server-port-pattern": "Port muss eine positive ganze Zahl sein.",
"server-port-range": "Port muss im Bereich von 1 bis 65535 liegen.",
"server-public-key": "Öffentlicher Schlüssel des Servers",
"server-public-key-required": "Öffentlicher Schlüssel des Servers ist erforderlich.",
"client-hold-off-time": "Wartezeit des Clients",
"client-hold-off-time-required": "Wartezeit des Clients ist erforderlich.",
"client-hold-off-time-pattern": "Wartezeit des Clients muss eine positive Ganzzahl sein.",
"client-hold-off-time-tooltip": "Client-Wartezeit nur bei Verwendung eines Bootstrap-Servers",
"account-after-timeout": "Konto nach Zeitüberschreitung",
"account-after-timeout-required": "Konto nach Zeitüberschreitung ist erforderlich.",
"account-after-timeout-pattern": "Konto nach Zeitüberschreitung muss eine positive Ganzzahl sein.",
"account-after-timeout-tooltip": "Wert für Konto nach Zeitüberschreitung beim Bootstrap-Server.",
"client-hold-off-time": "Verzögerungszeit (Hold Off Time)",
"client-hold-off-time-required": "Verzögerungszeit ist erforderlich.",
"client-hold-off-time-pattern": "Verzögerungszeit muss eine positive ganze Zahl sein.",
"client-hold-off-time-tooltip": "Verzögerungszeit für den Einsatz mit einem Bootstrap-Server",
"account-after-timeout": "Aktivierung nach Zeitüberschreitung",
"account-after-timeout-required": "Aktivierung nach Zeitüberschreitung ist erforderlich.",
"account-after-timeout-pattern": "Der Wert muss eine positive ganze Zahl sein.",
"account-after-timeout-tooltip": "Bootstrap-Server-Aktivierung nach dem durch diese Ressource angegebenen Timeout-Wert.",
"server-type": "Servertyp",
"add-new-server-title": "Neue Serverkonfiguration hinzufügen",
"add-server-config": "Serverkonfiguration hinzufügen",
@ -2267,25 +2286,25 @@
"others-tab": "Weitere Einstellungen",
"ota-update": "OTA-Update",
"use-object-19-for-ota-update": "Objekt 19 für OTA-Dateimetadaten verwenden (Prüfsumme, Größe, Version, Name)",
"use-object-19-for-ota-update-hint": "Verwenden Sie Resource ObjectId = 19 für OTA-Updates: Firmware → InstanceId = 65534, Software → InstanceId = 65535. Das Datenformat ist JSON, codiert in Base64. Dieses JSON enthält OTA-Dateimetadaten (Dateiinformationen): „Checksum“ (SHA256). Zusätzliche Felder: „Title“ (OTA-Name), „Version“ (OTA-Version), „File Name“ (Dateiname zur Speicherung auf dem Client), „File Size“ (OTA-Größe in Bytes).",
"client-strategy": "Client-Strategie beim Verbinden",
"use-object-19-for-ota-update-hint": "Verwenden Sie Resource ObjectId = 19 für OTA-Updates: FirmWare → InstanceId = 65534, SoftWare → InstanceId = 65535. Das Datenformat ist JSON, eingebettet in Base64. Dieses JSON enthält OTA-Dateimetadaten (Dateiinformationen): \"Prüfsumme\" (SHA256). Zusätzliche Felder: \"Titel\" (OTA-Name), \"Version\" (OTA-Version), \"Dateiname\" (Dateiname für die Speicherung auf dem Client), \"Dateigröße\" (OTA-Größe in Byte).",
"client-strategy": "Client-Strategie beim Verbindungsaufbau",
"client-strategy-label": "Strategie",
"client-strategy-only-observe": "Nur Beobachtungsanfragen nach der ersten Verbindung",
"client-strategy-read-all": "Alle Ressourcen lesen und beobachten nach der Registrierung",
"client-strategy-only-observe": "Nur Observe-Anfrage an den Client nach der ersten Verbindung",
"client-strategy-read-all": "Alle Ressourcen lesen und Observe-Anfrage nach der Registrierung senden",
"fw-update": "Firmware-Update",
"fw-update-strategy": "Firmware-Update-Strategie",
"fw-update-strategy-data": "Firmware-Update als Binärdatei mit Objekt 19 und Ressource 0 (Daten) übertragen",
"fw-update-strategy-package": "Firmware-Update als Binärdatei mit Objekt 5 und Ressource 0 (Paket) übertragen",
"fw-update-strategy-package-uri": "Automatisch eindeutige CoAP-URL generieren, um das Paket herunterzuladen, und als Objekt 5 und Ressource 1 (Paket-URI) übertragen",
"fw-update-strategy": "Strategie für Firmware-Update",
"fw-update-strategy-data": "Firmware-Update als Binärdatei über Objekt 19 und Ressource 0 (Data) übertragen",
"fw-update-strategy-package": "Firmware-Update als Binärdatei über Objekt 5 und Ressource 0 (Package) übertragen",
"fw-update-strategy-package-uri": "Automatisch eindeutige CoAP-URL generieren und Firmware-Update über Objekt 5 und Ressource 1 (Package URI) übertragen",
"sw-update": "Software-Update",
"sw-update-strategy": "Software-Update-Strategie",
"sw-update-strategy-package": "Binärdatei mit Objekt 9 und Ressource 2 (Paket) übertragen",
"sw-update-strategy-package-uri": "Automatisch eindeutige CoAP-URL generieren, um das Paket herunterzuladen, und Software-Update mit Objekt 9 und Ressource 3 (Paket-URI) übertragen",
"fw-update-resource": "Firmware-Update CoAP-Ressource",
"fw-update-resource-required": "Firmware-Update CoAP-Ressource ist erforderlich.",
"sw-update-resource": "Software-Update CoAP-Ressource",
"sw-update-resource-required": "Software-Update CoAP-Ressource ist erforderlich.",
"config-json-tab": "JSON-Konfigurationsprofil Gerät",
"sw-update-strategy": "Strategie für Software-Update",
"sw-update-strategy-package": "Binärdatei über Objekt 9 und Ressource 2 (Package) übertragen",
"sw-update-strategy-package-uri": "Automatisch eindeutige CoAP-URL generieren und Software-Update über Objekt 9 und Ressource 3 (Package URI) übertragen",
"fw-update-resource": "CoAP-Ressource für Firmware-Update",
"fw-update-resource-required": "CoAP-Ressource für Firmware-Update ist erforderlich.",
"sw-update-resource": "CoAP-Ressource für Software-Update",
"sw-update-resource-required": "CoAP-Ressource für Software-Update ist erforderlich.",
"config-json-tab": "JSON-Konfigurationsprofil für Gerät",
"attributes-name": {
"min-period": "Minimale Periode",
"max-period": "Maximale Periode",
@ -2810,20 +2829,20 @@
"server-attributes-placeholder": "Server-Attribute",
"timeseries-placeholder": "Zeitreihen",
"target-entity": "Zielentität",
"attributes-propagation": "Attributübertragung",
"attributes-propagation-hint": "Die Entitätsansicht kopiert automatisch die angegebenen Attribute von der Zielentität jedes Mal, wenn Sie diese Entitätsansicht speichern oder aktualisieren. Aus Leistungsgründen werden Zielentitätsattribute nicht bei jeder Attributänderung auf die Entitätsansicht übertragen. Sie können die automatische Übertragung aktivieren, indem Sie einen 'copy to view'-Regelknoten in Ihrer Regelkette konfigurieren und 'Post attributes' und 'Attributes Updated'-Nachrichten an diesen neuen Regelknoten verlinken.",
"attributes-propagation": "Attributweiterleitung",
"attributes-propagation-hint": "Die Entitätsansicht kopiert automatisch die angegebenen Attribute von der Zielentität jedes Mal, wenn Sie diese Entitätsansicht speichern oder aktualisieren. Aus Leistungsgründen werden die Attribute der Zielentität nicht bei jeder Attributänderung automatisch in die Entitätsansicht übernommen. Sie können eine automatische Weiterleitung aktivieren, indem Sie den Regelknoten \"In Ansicht kopieren\" in Ihrer Regelkette konfigurieren und die Nachrichten \"Post attributes\" und \"Attributes Updated\" mit diesem neuen Regelknoten verknüpfen.",
"timeseries-data": "Zeitreihendaten",
"timeseries-data-hint": "Konfigurieren Sie die Zeitreihendatenschlüssel der Zielentität, die für die Entitätsansicht zugänglich sein werden. Diese Zeitreihendaten sind schreibgeschützt.",
"search": "Entitätsansichten suchen",
"selected-entity-views": "{ count, plural, =1 {1 Entitätsansicht} other {# Entitätsansichten} } ausgewählt",
"assign-entity-view-to-edge": "Entitätsansicht(en) Edge zuweisen",
"assign-entity-view-to-edge-text": "Bitte wählen Sie die Entitätsansichten aus, die dem Edge zugewiesen werden sollen",
"unassign-entity-view-from-edge-title": "Sind Sie sicher, dass Sie die Entitätsansicht '{{entityViewName}}' von Edge entfernen möchten?",
"unassign-entity-view-from-edge-text": "Nach der Bestätigung wird die Entitätsansicht von Edge entfernt und ist nicht mehr zugänglich.",
"unassign-entity-views-from-edge-action-title": "{ count, plural, =1 {1 Entitätsansicht} other {# Entitätsansichten} } von Edge entfernen",
"unassign-entity-view-from-edge": "Entitätsansicht von Edge entfernen",
"unassign-entity-views-from-edge-title": "Sind Sie sicher, dass Sie { count, plural, =1 {1 Entitätsansicht} other {# Entitätsansichten} } von Edge entfernen möchten?",
"unassign-entity-views-from-edge-text": "Nach der Bestätigung werden alle ausgewählten Entitätsansichten von Edge entfernt und sind nicht mehr zugänglich."
"timeseries-data-hint": "Konfigurieren Sie die Zeitreihenschlüssel der Zielentität, die in der Entitätsansicht verfügbar sein sollen. Diese Zeitreihendaten sind schreibgeschützt.",
"search": "Entitätssichten suchen",
"selected-entity-views": "{ count, plural, =1 {1 Entitätssicht} other {# Entitätssichten} } ausgewählt",
"assign-entity-view-to-edge": "Entitätssicht(en) einer Edge zuweisen",
"assign-entity-view-to-edge-text": "Bitte wählen Sie die Entitätssichten aus, die der Edge zugewiesen werden sollen",
"unassign-entity-view-from-edge-title": "Möchten Sie die Entitätsansicht '{{entityViewName}}' wirklich von der Edge entfernen?",
"unassign-entity-view-from-edge-text": "Nach der Bestätigung wird die Entitätsansicht entfernt und ist auf der Edge nicht mehr verfügbar.",
"unassign-entity-views-from-edge-action-title": "{ count, plural, =1 {1 Entitätsansicht von der Edge entfernen} other {# Entitätssichten von der Edge entfernen} }",
"unassign-entity-view-from-edge": "Entitätsansicht entfernen",
"unassign-entity-views-from-edge-title": "Möchten Sie { count, plural, =1 {1 Entitätssicht} other {# Entitätssichten} } wirklich entfernen?",
"unassign-entity-views-from-edge-text": "Nach der Bestätigung werden alle ausgewählten Entitätssichten entfernt und sind auf der Edge nicht mehr verfügbar."
},
"event": {
"event-type": "Ereignistyp",
@ -3041,15 +3060,15 @@
"edit": "Filter bearbeiten",
"name": "Filtername",
"name-required": "Filtername ist erforderlich.",
"duplicate-filter": "Ein Filter mit demselben Namen existiert bereits.",
"duplicate-filter": "Ein Filter mit demselben Namen ist bereits vorhanden.",
"filters": "Filter",
"unable-delete-filter-title": "Filter kann nicht gelöscht werden",
"unable-delete-filter-text": "Der Filter '{{filter}}' kann nicht gelöscht werden, da er von folgenden Widget(s) verwendet wird:<br/>{{widgetsList}}",
"duplicate-filter-error": "Doppelter Filter gefunden '{{filter}}'.<br>Filter müssen innerhalb des Dashboards eindeutig sein.",
"unable-delete-filter-text": "Filter '{{filter}}' kann nicht gelöscht werden, da er von folgendem/n Widget(s) verwendet wird:<br/>{{widgetsList}}",
"duplicate-filter-error": "Doppelter Filter gefunden: '{{filter}}'.<br>Filter müssen innerhalb des Dashboards eindeutig sein.",
"missing-key-filters-error": "Schlüsselfilter fehlt für Filter '{{filter}}'.",
"filter": "Filter",
"editable": "Bearbeitbar",
"editable-hint": "Benutzern erlauben, den Filterwert in Dashboards zu ändern.",
"editable-hint": "Ermöglicht dem Benutzer, den Filterwert in Dashboards zu ändern.",
"no-filters-found": "Keine Filter gefunden.",
"no-filter-text": "Kein Filter angegeben",
"add-filter-prompt": "Bitte Filter hinzufügen",
@ -4303,7 +4322,7 @@
},
"profile": {
"profile": "Profil",
"last-login-time": "Letzte Anmeldung",
"last-login-time": "Letzter Login",
"change-password": "Passwort ändern",
"current-password": "Aktuelles Passwort",
"copy-jwt-token": "JWT-Token kopieren",
@ -4478,7 +4497,8 @@
"jks": "JKS",
"js-module": "JS-Modul",
"lwm2m-model": "LWM2M-Modell",
"pkcs-12": "PKCS #12"
"pkcs-12": "PKCS #12",
"general": "Allgemein"
},
"resource-sub-type": "Subtyp",
"sub-type": {
@ -4486,7 +4506,12 @@
"scada-symbol": "SCADA-Symbol",
"extension": "Erweiterung",
"module": "Modul"
}
},
"resource-is-in-use": "Ressource wird von anderen Entitäten verwendet",
"resources-are-in-use": "Ressourcen werden von anderen Entitäten verwendet",
"resource-is-in-use-text": "Die Ressource <b>'{{title}}'</b> wurde nicht gelöscht, da sie von folgenden Entitäten verwendet wird:",
"resources-are-in-use-text": "Nicht alle Ressourcen wurden gelöscht, da sie von anderen Entitäten verwendet werden.</br>Sie können die referenzierten Entitäten anzeigen, indem Sie auf die Schaltfläche <b>Referenzen</b> in der entsprechenden Ressourcenzeile klicken.</br>Wenn Sie diese Ressourcen dennoch löschen möchten, wählen Sie sie in der Tabelle unten aus und klicken Sie auf die Schaltfläche <b>Ausgewählte löschen</b>.",
"delete-resource-in-use-text": "Wenn Sie die Ressource dennoch löschen möchten, klicken Sie auf die Schaltfläche <b>Trotzdem löschen</b>."
},
"javascript": {
"add": "JavaScript-Ressource hinzufügen",
@ -4742,24 +4767,24 @@
"send-attributes-deleted-notification": "Benachrichtigung bei gelöschten Attributen senden",
"send-attributes-deleted-notification-hint": "Sendet Benachrichtigung über gelöschte Attribute als separate Nachricht an die Regelmaschinen-Warteschlange.",
"update-attributes-only-on-value-change": "Attribute nur bei Wertänderung speichern",
"update-attributes-only-on-value-change-hint": "Aktualisiert Attribute bei jeder eingehenden Nachricht, unabhängig von Wertänderung. Erhöht API-Nutzung und verringert Leistung.",
"update-attributes-only-on-value-change-hint-enabled": "Aktualisiert Attribute nur bei Wertänderung. Keine Aktualisierung von Zeitstempel oder Änderungsbenachrichtigung bei gleichem Wert.",
"update-attributes-only-on-value-change-hint": "Aktualisiert die Attribute bei jeder eingehenden Nachricht, unabhängig davon, ob sich deren Wert geändert hat. Erhöht die API-Nutzung und verringert die Leistung.",
"update-attributes-only-on-value-change-hint-enabled": "Aktualisiert die Attribute nur, wenn sich deren Wert geändert hat. Wenn der Wert unverändert bleibt, werden weder der Attribut-Zeitstempel noch eine Änderungsbenachrichtigung gesendet.",
"fetch-credentials-to-metadata": "Zugangsdaten in Metadaten übernehmen",
"notify-device-on-update-hint": "Wenn aktiviert, erzwingt Benachrichtigung an das Gerät bei Attribut-Update. Bei Deaktivierung erfolgt Steuerung über Metadatenparameter 'notifyDevice'.",
"notify-device-on-delete-hint": "Wenn aktiviert, erzwingt Benachrichtigung an das Gerät bei Attribut-Entfernung. Bei Deaktivierung erfolgt Steuerung über Metadatenparameter 'notifyDevice'.",
"latest-timeseries": "Neueste Zeitreihenschlüssel",
"notify-device-on-update-hint": "Wenn aktiviert, wird das Gerät über die Aktualisierung gemeinsamer Attribute zwangsweise benachrichtigt. Wenn deaktiviert, steuert das Metadatenfeld 'notifyDevice' im eingehenden Nachrichtenkontext das Benachrichtigungsverhalten. Um die Benachrichtigung zu deaktivieren, muss der Parameter 'notifyDevice' auf 'false' gesetzt sein. In allen anderen Fällen wird eine Benachrichtigung ausgelöst.",
"notify-device-on-delete-hint": "Wenn aktiviert, wird das Gerät über das Entfernen gemeinsamer Attribute zwangsweise benachrichtigt. Wenn deaktiviert, steuert das Metadatenfeld 'notifyDevice' im eingehenden Nachrichtenkontext das Verhalten. Um die Benachrichtigung zu aktivieren, muss der Parameter 'notifyDevice' auf 'true' gesetzt sein. In allen anderen Fällen wird keine Benachrichtigung gesendet.",
"latest-timeseries": "Neueste Zeitreihendatenschlüssel",
"timeseries-keys": "Zeitreihenschlüssel",
"timeseries-keys-required": "Mindestens ein Zeitreihenschlüssel muss ausgewählt werden.",
"timeseries-keys-required": "Mindestens ein Zeitreihenschlüssel muss ausgewählt sein.",
"add-timeseries-key": "Zeitreihenschlüssel hinzufügen",
"add-message-field": "Nachrichtenfeld hinzufügen",
"relation-search-parameters": "Beziehungs-Suchparameter",
"relation-search-parameters": "Suchparameter für Beziehungen",
"relation-parameters": "Beziehungsparameter",
"add-metadata-field": "Metadatenfeld hinzufügen",
"data-keys": "Feldnamen der Nachricht",
"copy-from": "Kopieren von",
"data-to-metadata": "Daten zu Metadaten",
"metadata-to-data": "Metadaten zu Daten",
"use-regular-expression-hint": "Verwenden Sie reguläre Ausdrücke, um Schlüssel anhand eines Musters zu kopieren.\n\nTipps & Tricks:\nDrücken Sie 'Enter', um die Eingabe des Feldnamens abzuschließen.\nDrücken Sie 'Rücktaste', um den Feldnamen zu löschen. Mehrere Feldnamen werden unterstützt.",
"use-regular-expression-hint": "Verwenden Sie reguläre Ausdrücke, um Schlüssel anhand von Mustern zu kopieren.\n\nTipps & Tricks:\nDrücken Sie 'Enter', um die Eingabe eines Feldnamens abzuschließen.\nDrücken Sie 'Rücktaste', um einen Feldnamen zu löschen. Mehrere Feldnamen werden unterstützt.",
"interval": "Intervall",
"interval-required": "Intervall ist erforderlich",
"interval-hint": "Deduplizierungsintervall in Sekunden.",
@ -4826,13 +4851,13 @@
"fields-mapping-hint": "Wenn das Nachrichtenfeld auf $entityId gesetzt ist, wird die ID des Absenders in die angegebene Spalte gespeichert.",
"relations-query-config-direction-suffix": "Absender",
"profile-name": "Profilname",
"fetch-circle-parameter-info-from-metadata-hint": "Das Metadatenfeld '{{perimeterKeyName}}' sollte folgendes Format haben: {\"latitude\":48.196, \"longitude\":24.6532, \"radius\":100.0, \"radiusUnit\":\"METER\"}",
"fetch-poligon-parameter-info-from-metadata-hint": "Das Metadatenfeld '{{perimeterKeyName}}' sollte folgendes Format haben: [[48.19736,24.65235],[48.19800,24.65060],...,[48.19849,24.65420]]",
"short-templatization-tooltip": "Verwenden Sie $[messageKey] für Nachrichtenwerte und ${metadataKey} für Metadatenwerte.",
"fetch-circle-parameter-info-from-metadata-hint": "Das Metadatenfeld '{{perimeterKeyName}}' muss im folgenden Format definiert sein: {\"latitude\":48.196, \"longitude\":24.6532, \"radius\":100.0, \"radiusUnit\":\"METER\"}",
"fetch-poligon-parameter-info-from-metadata-hint": "Das Metadatenfeld '{{perimeterKeyName}}' muss im folgenden Format definiert sein: [[48.19736,24.65235],[48.19800,24.65060],...,[48.19849,24.65420]]",
"short-templatization-tooltip": "Verwenden Sie $[messageKey], um einen Wert aus der Nachricht zu extrahieren, und ${metadataKey}, um einen Wert aus den Metadaten zu extrahieren.",
"fields-mapping-required": "Mindestens eine Feldzuordnung muss angegeben werden.",
"at-least-one-field-required": "Mindestens ein Eingabefeld muss einen Wert enthalten.",
"originator-fields-sv-map-hint": "Zielschlüsselfelder unterstützen Templatisierung. Verwenden Sie $[messageKey] oder ${metadataKey}.",
"sv-map-hint": "Nur Zielschlüsselfelder unterstützen Templatisierung. Verwenden Sie $[messageKey] oder ${metadataKey}.",
"originator-fields-sv-map-hint": "Zielschlüsselfelder unterstützen Templatisierung. Verwenden Sie $[messageKey], um einen Wert aus der Nachricht zu extrahieren, und ${metadataKey}, um einen Wert aus den Metadaten zu extrahieren.",
"sv-map-hint": "Nur Zielschlüsselfelder unterstützen Templatisierung. Verwenden Sie $[messageKey], um einen Wert aus der Nachricht zu extrahieren, und ${metadataKey}, um einen Wert aus den Metadaten zu extrahieren.",
"source-field": "Quellfeld",
"source-field-required": "Quellfeld ist erforderlich.",
"originator-source": "Absenderquelle",
@ -5436,11 +5461,11 @@
"prompt-settings": "Prompt-Einstellungen",
"prompt-settings-hint": "Der optionale System-Prompt definiert die allgemeine Rolle und Einschränkungen der KI, während der Benutzer-Prompt die spezifische Aufgabe beschreibt. Beide Felder unterstützen auch die Verwendung von Templates.",
"system-prompt": "System-Prompt",
"system-prompt-max-length": "Der System-Prompt darf maximal 500.000 Zeichen lang sein.",
"system-prompt-max-length": "Der System-Prompt darf maximal 10.000 Zeichen lang sein.",
"system-prompt-blank": "Der System-Prompt darf nicht leer sein.",
"user-prompt": "Benutzer-Prompt",
"user-prompt-required": "Benutzer-Prompt ist erforderlich.",
"user-prompt-max-length": "Der Benutzer-Prompt darf maximal 500.000 Zeichen lang sein.",
"user-prompt-max-length": "Der Benutzer-Prompt darf maximal 10.000 Zeichen lang sein.",
"user-prompt-blank": "Der Benutzer-Prompt darf nicht leer sein.",
"response-format": "Antwortformat",
"response-text": "Text",
@ -5457,7 +5482,8 @@
"timeout-required": "Zeitüberschreitung ist erforderlich",
"timeout-validation": "Muss zwischen 1 Sekunde und 10 Minuten liegen.",
"force-acknowledgement": "Erzwinge Bestätigung",
"force-acknowledgement-hint": "Wenn aktiviert, wird die eingehende Nachricht sofort bestätigt. Die Antwort des Modells wird dann als separate, neue Nachricht eingereiht."
"force-acknowledgement-hint": "Wenn aktiviert, wird die eingehende Nachricht sofort bestätigt. Die Antwort des Modells wird dann als separate, neue Nachricht eingereiht.",
"ai-resources": "KI-Ressourcen"
}
},
"timezone": {
@ -5987,9 +6013,10 @@
"label": "Beschriftung",
"value": "Wert",
"date": "Datum",
"show-date-time-interval": "Datum/Zeit-Intervall anzeigen",
"show-date-time-interval-hint": "Datum/Zeit-Intervall gemäß Datenaggregation anzeigen.",
"show-date-time-interval": "Datums-/Zeitintervall anzeigen",
"show-date-time-interval-hint": "Zeigt das Datums-/Zeitintervall entsprechend der Datenaggregation an.",
"hide-zero-tooltip-values": "Nullwerte ausblenden",
"show-stack-total": "Gesamtwert im Stapelmodus anzeigen",
"background-color": "Hintergrundfarbe",
"background-blur": "Hintergrundunschärfe"
},
@ -6556,10 +6583,10 @@
"no-users-text": "Keine Benutzer gefunden",
"user-details": "Benutzerdetails",
"delete-user-title": "Möchten Sie den Benutzer '{{userEmail}}' wirklich löschen?",
"delete-user-text": "Vorsicht! Nach der Bestätigung werden der Benutzer und alle zugehörigen Daten unwiederbringlich gelöscht.",
"delete-users-title": "Möchten Sie wirklich { count, plural, =1 {1 Benutzer} other {# Benutzer} } löschen?",
"delete-user-text": "Achtung: Nach der Bestätigung werden der Benutzer und alle zugehörigen Daten unwiederbringlich gelöscht.",
"delete-users-title": "Möchten Sie { count, plural, =1 {1 Benutzer} other {# Benutzer} } wirklich löschen?",
"delete-users-action-title": "{ count, plural, =1 {1 Benutzer löschen} other {# Benutzer löschen} }",
"delete-users-text": "Vorsicht! Nach der Bestätigung werden alle ausgewählten Benutzer und zugehörige Daten gelöscht.",
"delete-users-text": "Achtung: Nach der Bestätigung werden alle ausgewählten Benutzer und ihre zugehörigen Daten unwiederbringlich entfernt.",
"activation-email-sent-message": "Aktivierungs-E-Mail wurde erfolgreich gesendet!",
"resend-activation": "Aktivierung erneut senden",
"email": "E-Mail",
@ -6669,24 +6696,24 @@
"remove-all": "Alle entfernen",
"version-create-result": "{ added, plural, =0 {Keine Entitäten} =1 {1 Entität} other {# Entitäten} } hinzugefügt.<br/>{ modified, plural, =0 {Keine Entitäten} =1 {1 Entität} other {# Entitäten} } geändert.<br/>{ removed, plural, =0 {Keine Entitäten} =1 {1 Entität} other {# Entitäten} } entfernt.",
"remove-other-entities": "Andere Entitäten entfernen",
"find-existing-entity-by-name": "Vorhandene Entität nach Namen finden",
"find-existing-entity-by-name": "Bestehende Entität nach Name suchen",
"restore-entities-from-version": "Entitäten aus Version '{{versionName}}' wiederherstellen",
"restoring-entities-from-version": "Entitäten werden wiederhergestellt... Bitte warten",
"no-entities-restored": "Keine Entitäten wiederhergestellt",
"no-entities-restored": "Keine Entitäten wurden wiederhergestellt",
"created": "{{created}} erstellt",
"updated": "{{updated}} aktualisiert",
"deleted": "{{deleted}} gelöscht",
"remove-other-entities-confirm-text": "Achtung! Dies wird <b>alle</b> aktuellen Entitäten dauerhaft <b>löschen</b><br/>die nicht in der wiederherzustellenden Version vorhanden sind.<br/><br/>Bitte geben Sie <b>\"andere Entitäten entfernen\"</b> ein, um zu bestätigen.",
"auto-commit-to-branch": "automatisch in <b>{{ branch }}</b>-Zweig übernehmen",
"default-create-entity-version-name": "{{entityName}} Aktualisierung",
"sync-strategy-merge-hint": "Erstellt oder aktualisiert ausgewählte Entitäten im Repository. Alle anderen Repository-Entitäten werden <b>nicht verändert</b>.",
"sync-strategy-overwrite-hint": "Erstellt oder aktualisiert ausgewählte Entitäten im Repository. Alle anderen Repository-Entitäten werden <b>gelöscht</b>.",
"device-credentials-conflict": "Gerät mit externer ID <b>{{entityId}}</b> konnte nicht geladen werden,<br/>da dieselben Zugangsdaten bereits für ein anderes Gerät vorhanden sind.<br/>Bitte erwägen Sie, die Einstellung <b>Zugangsdaten laden</b> im Wiederherstellungsformular zu deaktivieren.",
"missing-referenced-entity": "<b>{{sourceEntityTypeName}}</b> mit externer ID <b>{{sourceEntityId}}</b> konnte nicht geladen werden,<br/>da sie auf eine fehlende <b>{{targetEntityTypeName}}</b> mit ID <b>{{targetEntityId}}</b> verweist.",
"remove-other-entities-confirm-text": "Achtung! Dies wird <b>alle</b> aktuellen Entitäten <b>dauerhaft löschen</b>,<br/>die nicht in der wiederherzustellenden Version vorhanden sind.<br/><br/>Bitte geben Sie <b>\"remove other entities\"</b> ein, um zu bestätigen.",
"auto-commit-to-branch": "Auto-Commit in den Branch <b>{{branch}}</b>",
"default-create-entity-version-name": "{{entityName}}-Aktualisierung",
"sync-strategy-merge-hint": "Erstellt oder aktualisiert die ausgewählten Entitäten im Repository. Alle anderen Repository-Entitäten werden <b>nicht verändert</b>.",
"sync-strategy-overwrite-hint": "Erstellt oder aktualisiert die ausgewählten Entitäten im Repository. Alle anderen Repository-Entitäten werden <b>gelöscht</b>.",
"device-credentials-conflict": "Laden des Geräts mit externer ID <b>{{entityId}}</b> fehlgeschlagen,<br/>da dieselben Zugangsdaten bereits für ein anderes Gerät in der Datenbank vorhanden sind.<br/>Deaktivieren Sie ggf. die Option <b>Zugangsdaten laden</b> im Wiederherstellungsformular.",
"missing-referenced-entity": "Laden von <b>{{sourceEntityTypeName}}</b> mit externer ID <b>{{sourceEntityId}}</b> fehlgeschlagen,<br/>da die referenzierte <b>{{targetEntityTypeName}}</b> mit ID <b>{{targetEntityId}}</b> fehlt.",
"runtime-failed": "<b>Fehlgeschlagen:</b> {{message}}",
"auto-commit-settings-read-only-hint": "Auto-Commit funktioniert nicht, wenn die Option 'Nur-Lesen' in den Repository-Einstellungen aktiviert ist.",
"rollback-on-error": "Bei Fehler zurücksetzen",
"rollback-on-error-hint": "Wenn Sie eine große Menge an Entitäten wiederherstellen, deaktivieren Sie diese Option zur Leistungssteigerung.\nBeachten Sie, dass bei einem Fehler bereits gespeicherte Entitäten (mit Beziehungen, Attributen etc.) erhalten bleiben."
"auto-commit-settings-read-only-hint": "Die Auto-Commit-Funktion funktioniert nicht, wenn die Option 'Nur Lesen' in den Repository-Einstellungen aktiviert ist.",
"rollback-on-error": "Rollback bei Fehlern",
"rollback-on-error-hint": "Wenn Sie eine große Anzahl von Entitäten wiederherstellen, sollten Sie diese Option zur Leistungsverbesserung deaktivieren.\nHinweis: Im Fehlerfall bleiben bereits gespeicherte Entitäten (mit Beziehungen, Attributen usw.) erhalten."
},
"widget": {
"widget-library": "Widget-Bibliothek",
@ -6703,8 +6730,8 @@
"management": "Widget-Verwaltung",
"editor": "Widget-Editor",
"confirm-to-exit-editor-html": "Sie haben nicht gespeicherte Widget-Einstellungen.<br>Möchten Sie diese Seite wirklich verlassen?",
"widget-type-not-found": "Fehler beim Laden der Widget-Konfiguration.<br>Vermutlich wurde der zugehörige Widget-Typ entfernt.",
"widget-type-load-error": "Widget konnte aufgrund folgender Fehler nicht geladen werden:",
"widget-type-not-found": "Fehler beim Laden der Widget-Konfiguration.<br>Der zugehörige\n Widget-Typ wurde möglicherweise entfernt.",
"widget-type-load-error": "Widget konnte aufgrund der folgenden Fehler nicht geladen werden:",
"remove": "Widget entfernen",
"delete": "Widget löschen",
"edit": "Widget bearbeiten",
@ -6769,11 +6796,11 @@
"javascript": "JavaScript",
"js": "JS",
"delete-widget-title": "Möchten Sie das Widget '{{widgetName}}' wirklich löschen?",
"delete-widget-text": "Nach der Bestätigung wird das Widget und alle zugehörigen Daten unwiederbringlich gelöscht.",
"delete-widgets-title": "Möchten Sie wirklich { count, plural, =1 {1 Widget} other {# Widgets} } löschen?",
"delete-widgets-text": "Nach der Bestätigung werden alle ausgewählten Widgets und alle zugehörigen Daten gelöscht.",
"delete-widget-text": "Nach der Bestätigung wird das Widget und alle zugehörigen Daten unwiderruflich gelöscht.",
"delete-widgets-title": "Möchten Sie { count, plural, =1 {1 Widget} other {# Widgets} } wirklich löschen?",
"delete-widgets-text": "Achtung: Nach der Bestätigung werden alle ausgewählten Widgets und deren zugehörige Daten unwiderruflich entfernt.",
"delete-widget": "Widget löschen",
"widget-template-load-failed-error": "Widget-Vorlage konnte nicht geladen werden!",
"widget-template-load-failed-error": "Laden der Widget-Vorlage fehlgeschlagen!",
"details": "Details",
"widget-details": "Widget-Details",
"add": "Widget hinzufügen",
@ -8572,15 +8599,15 @@
"fit-map-bounds": "Kartenausschnitt anpassen, um alle Marker abzudecken",
"use-default-map-center-position": "Standard-Zentrum der Karte verwenden",
"entities-limit": "Grenze für zu ladende Entitäten",
"markers-settings": "Marker-Einstellungen",
"marker-offset-x": "X-Versatz des Markers multipliziert mit Breite",
"marker-offset-y": "Y-Versatz des Markers multipliziert mit Höhe",
"position-function": "Positionsumrechnungsfunktion, sollte x,y-Koordinaten als Double (0-1) zurückgeben",
"draggable-marker": "Marker verschiebbar",
"markers-settings": "Markereinstellungen",
"marker-offset-x": "Marker-X-Versatz relativ zur Position multipliziert mit der Markerbreite",
"marker-offset-y": "Marker-Y-Versatz relativ zur Position multipliziert mit der Markerhöhe",
"position-function": "Positionskonvertierungsfunktion, sollte x,y-Koordinaten im Bereich von 0 bis 1 (double) zurückgeben",
"draggable-marker": "Verschiebbarer Marker",
"label": "Beschriftung",
"show-label": "Beschriftung anzeigen",
"use-label-function": "Beschriftungsfunktion verwenden",
"label-pattern": "Beschriftung (z.B.: '${entityName}', '${entityName}: (Text ${keyName} Einheiten.)')",
"label-pattern": "Beschriftung (Beispielmuster: '${entityName}', '${entityName}: (Text ${keyName} Einheiten.)')",
"label-function": "Beschriftungsfunktion",
"tooltip": "Tooltip",
"show-tooltip": "Tooltip anzeigen",
@ -9522,4 +9549,4 @@
"language": {
"language": "Sprache"
}
}
}

681
ui-ngx/src/assets/locale/locale.constant-el_GR.json

File diff suppressed because it is too large

8
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -5948,13 +5948,13 @@
"timewindow": {
"timewindow": "Time window",
"timewindow-settings": "Time window settings",
"years": "{ years, plural, =1 { year } other {# years } }",
"years": "{ years, plural, =1 {1 year } other {# years } }",
"years-short": "{{ years }}y",
"months": "{ months, plural, =1 { month } other {# months } }",
"months": "{ months, plural, =1 {1 month } other {# months } }",
"months-short": "{{ months }}M",
"weeks": "{ weeks, plural, =1 { week } other {# weeks } }",
"weeks": "{ weeks, plural, =1 {1 week } other {# weeks } }",
"weeks-short": "{{ weeks }}w",
"days": "{ days, plural, =1 { day } other {# days } }",
"days": "{ days, plural, =1 {1 day } other {# days } }",
"days-short": "{{ days }}d",
"hours": "{ hours, plural, =0 { hour } =1 {1 hour } other {# hours } }",
"hr": "{{ hr }} hr",

1449
ui-ngx/src/assets/locale/locale.constant-fr_FR.json

File diff suppressed because it is too large

1084
ui-ngx/src/assets/locale/locale.constant-it_IT.json

File diff suppressed because it is too large

1515
ui-ngx/src/assets/locale/locale.constant-nl_NL.json

File diff suppressed because it is too large

78
ui-ngx/yarn.lock

@ -5604,11 +5604,6 @@ fs-minipass@^3.0.0:
dependencies:
minipass "^7.0.3"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@^2.3.2, fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
@ -5738,10 +5733,10 @@ glob-to-regexp@^0.4.1:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@^10.2.2, glob@^10.3.10, glob@^10.3.3, glob@^10.3.7:
version "10.4.5"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
glob@10.5.0, glob@^10.2.2, glob@^10.3.10, glob@^10.3.3, glob@^10.3.7, glob@^7.1.3:
version "10.5.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c"
integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==
dependencies:
foreground-child "^3.1.0"
jackspeak "^3.1.2"
@ -5750,18 +5745,6 @@ glob@^10.2.2, glob@^10.3.10, glob@^10.3.3, glob@^10.3.7:
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
glob@^7.1.3:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.1.1"
once "^1.3.0"
path-is-absolute "^1.0.0"
global-prefix@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-4.0.0.tgz#e9cc79aab9be1d03287e156a3f912dd0895463ed"
@ -6104,24 +6087,16 @@ indent-string@^4.0.0:
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ini@4.1.3, ini@^4.1.3:
version "4.1.3"
resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.3.tgz#4c359675a6071a46985eb39b14e4a2c0ec98a795"
@ -7170,7 +7145,7 @@ minimatch@9.0.1:
dependencies:
brace-expansion "^2.0.1"
minimatch@^3.1.1, minimatch@^3.1.2:
minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@ -7493,10 +7468,10 @@ node-fetch@^3.3.2:
fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10"
node-forge@^1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
node-forge@1.3.3, node-forge@^1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751"
integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==
node-gyp-build-optional-packages@5.2.2:
version "5.2.2"
@ -7711,13 +7686,6 @@ on-headers@~1.0.2:
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies:
wrappy "1"
onetime@^5.1.0, onetime@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
@ -7982,11 +7950,6 @@ path-exists@^5.0.0:
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7"
integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
@ -8005,10 +7968,10 @@ path-scurry@^1.11.1:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
path-to-regexp@0.1.10:
version "0.1.10"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b"
integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==
path-to-regexp@0.1.10, path-to-regexp@0.1.12:
version "0.1.12"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
path-type@^5.0.0:
version "5.0.0"
@ -10079,11 +10042,6 @@ wrap-ansi@^9.0.0:
string-width "^7.0.0"
strip-ansi "^7.1.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@^8.16.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"

Loading…
Cancel
Save