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> <artifactId>rocksdbjni</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId> <artifactId>langchain4j-open-ai</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-azure-open-ai</artifactId> <artifactId>langchain4j-azure-open-ai</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-google-ai-gemini</artifactId> <artifactId>langchain4j-google-ai-gemini</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-vertex-ai-gemini</artifactId> <artifactId>langchain4j-vertex-ai-gemini</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-mistral-ai</artifactId> <artifactId>langchain4j-mistral-ai</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId> <artifactId>langchain4j-anthropic</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-bedrock</artifactId> <artifactId>langchain4j-bedrock</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-github-models</artifactId> <artifactId>langchain4j-github-models</artifactId>
<exclusions> <exclusions>
<exclusion> <exclusion>
@ -420,7 +420,7 @@
</exclusions> </exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId> <artifactId>langchain4j-ollama</artifactId>
</dependency> </dependency>
</dependencies> </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 #### 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 ```bash
sudo yum install -y nano wget && sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm 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) #### 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 ```bash
sudo dnf install java-17-openjdk sudo dnf install java-17-openjdk
{:copy-code} {:copy-code}
``` ```
Please don't forget to configure your operating system to use OpenJDK 17 by default. Configure your operating system to use OpenJDK 17 by default. You can configure the default version by running the following command:
You can configure which version is the default using the following command:
```bash ```bash
sudo update-alternatives --config java sudo update-alternatives --config java
{:copy-code} {: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 ```bash
java -version java -version
{:copy-code} {:copy-code}
``` ```
Expected command output is: The expected result is:
```text ```text
openjdk version "17.x.xx" openjdk version "17.x.xx"
@ -39,14 +38,13 @@ OpenJDK Runtime Environment (...)
OpenJDK 64-Bit Server VM (build ...) OpenJDK 64-Bit Server VM (build ...)
``` ```
#### Step 2. Configure ThingsBoard Database #### 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.
### PostgresSql ThingsBoard Edge supports **SQL** and **hybrid** database configurations.
ThingsBoard Edge uses PostgreSQL database as a local storage. In this guide, we’ll use an **SQL** database.
To install PostgreSQL, follow the instructions below. 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 ```bash
# Update your system # Update your system
@ -55,7 +53,8 @@ sudo dnf update
``` ```
Install the repository RPM: Install the repository RPM:
**For CentOS/RHEL 8:**
* **For CentOS/RHEL 8:**
```bash ```bash
# Install the repository RPM (For CentOS/RHEL 8): # 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} {:copy-code}
``` ```
**For CentOS/RHEL 9:** * **For CentOS/RHEL 9:**
```bash ```bash
# Install the repository RPM (for CentOS 9): # 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} {:copy-code}
``` ```
#### Step 3. Choose Queue Service #### Step 3. ThingsBoard Edge Service Installation
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
Download installation package: Download installation package:
```bash ```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} {:copy-code}
``` ```
@ -137,8 +125,8 @@ sudo rpm -Uvh tb-edge-${TB_EDGE_TAG}.rpm
{:copy-code} {:copy-code}
``` ```
#### Step 5. Configure ThingsBoard Edge #### Step 4. Configure ThingsBoard Edge
To configure ThingsBoard Edge, you can use the following command to automatically update the configuration file with specific values: To configure ThingsBoard Edge, you can use the following command to automatically update the configuration file with specific values:
```bash ```bash
sudo sh -c 'cat <<EOL >> /etc/tb-edge/conf/tb-edge.conf sudo sh -c 'cat <<EOL >> /etc/tb-edge/conf/tb-edge.conf
@ -151,7 +139,7 @@ EOL'
{:copy-code} {:copy-code}
``` ```
##### Configure PostgreSQL (Optional) ##### [Optional] Configure PostgreSQL Connection
If you changed PostgreSQL default datasource settings, use the following command: If you changed PostgreSQL default datasource settings, use the following command:
```bash ```bash
@ -163,11 +151,10 @@ EOL'
{:copy-code} {: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 ##### [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. 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**): 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} {: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 ```bash
sudo /usr/share/tb-edge/bin/install/install.sh sudo /usr/share/tb-edge/bin/install/install.sh
{:copy-code} {:copy-code}
``` ```
#### Step 7. Restart ThingsBoard Edge Service #### Step 6. Start ThingsBoard Edge Service
```bash ```bash
sudo service tb-edge restart sudo service tb-edge start
{:copy-code} {:copy-code}
``` ```
#### Step 8. Open ThingsBoard Edge UI #### Step 7. 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
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 #### 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>. 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> Now, create a Docker Compose file for the ThingsBoard Edge service:
#### 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
```bash ```bash
nano docker-compose.yml nano docker-compose.yml
@ -38,7 +21,6 @@ nano docker-compose.yml
Add the following lines to the yml file: Add the following lines to the yml file:
```bash ```bash
version: '3.8'
services: services:
mytbedge: mytbedge:
restart: always restart: always
@ -54,6 +36,7 @@ services:
CLOUD_RPC_HOST: ${BASE_URL} CLOUD_RPC_HOST: ${BASE_URL}
CLOUD_RPC_PORT: ${CLOUD_RPC_PORT} CLOUD_RPC_PORT: ${CLOUD_RPC_PORT}
CLOUD_RPC_SSL_ENABLED: ${CLOUD_RPC_SSL_ENABLED} CLOUD_RPC_SSL_ENABLED: ${CLOUD_RPC_SSL_ENABLED}
${EXTRA_HOSTS}
volumes: volumes:
- tb-edge-data:/data - tb-edge-data:/data
- tb-edge-logs:/var/log/tb-edge - tb-edge-logs:/var/log/tb-edge
@ -78,41 +61,28 @@ volumes:
{:copy-code} {:copy-code}
``` ```
##### [Optional] Update bind ports ##### [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. 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: Then, update the port configuration in the `docker-compose.yml` file accordingly:
```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:
```bash ```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} {:copy-code}
``` ```
###### NOTE: Docker Compose V2 vs docker-compose (with a hyphen) #### 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:
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:
```bash ```bash
docker-compose up -d docker compose up -d && docker compose logs -f mytbedge
docker-compose logs -f mytbedge {:copy-code}
``` ```
#### Step 3. Open ThingsBoard Edge UI #### Step 3. Open ThingsBoard Edge UI
Once the Edge service is started, open the Edge UI at http://localhost:8080. 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.
###### 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.
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) #### Step 1. Install Java 17 (OpenJDK)
ThingsBoard service is running on Java 17. To install OpenJDK 17, follow these instructions: ThingsBoard service is running on Java 17. To install OpenJDK 17, follow these instructions:
@ -25,19 +25,16 @@ java -version
The expected result is: The expected result is:
```text ```text
openjdk version "17.x.xx" openjdk version "17.x.xx"
OpenJDK Runtime Environment (...) OpenJDK Runtime Environment (...)
OpenJDK 64-Bit Server VM (...) OpenJDK 64-Bit Server VM (build ...)
``` ```
#### Step 2. Configure ThingsBoard Edge Database #### Step 2. Configure ThingsBoard Edge Database
ThingsBoard Edge supports SQL and hybrid database approaches. ThingsBoard Edge supports **SQL** and **hybrid** database configurations.
In this guide we will use SQL only. In this guide, we’ll use an **SQL** database.
For hybrid details please follow official installation instructions from the ThingsBoard documentation site. 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>.
### Configure PostgreSQL
ThingsBoard Edge uses PostgreSQL database as a local storage.
To install the PostgreSQL database, run these commands: 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} {:copy-code}
``` ```
#### Step 3. Choose Queue Service #### Step 3. ThingsBoard Edge Service Installation
Download the installation package:
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:
```bash ```bash
wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.deb 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} {:copy-code}
``` ```
#### Step 5. Configure ThingsBoard Edge #### Step 4. Configure ThingsBoard Edge
To configure ThingsBoard Edge, you can use the following command to automatically update the configuration file with specific values: To configure ThingsBoard Edge, you can use the following command to automatically update the configuration file with specific values:
```bash ```bash
sudo sh -c 'cat <<EOL >> /etc/tb-edge/conf/tb-edge.conf sudo sh -c 'cat <<EOL >> /etc/tb-edge/conf/tb-edge.conf
@ -110,7 +97,7 @@ EOL'
{:copy-code} {:copy-code}
``` ```
##### [Optional] Configure PostgreSQL ##### [Optional] Configure PostgreSQL Connection
If you changed PostgreSQL default datasource settings, use the following command: If you changed PostgreSQL default datasource settings, use the following command:
```bash ```bash
@ -122,10 +109,10 @@ EOL'
{:copy-code} {: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 ##### [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. 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**): 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} {: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 ```bash
sudo /usr/share/tb-edge/bin/install/install.sh sudo /usr/share/tb-edge/bin/install/install.sh
{:copy-code} {:copy-code}
``` ```
#### Step 7. Restart ThingsBoard Edge Service #### Step 6. Start ThingsBoard Edge Service
```bash ```bash
sudo service tb-edge restart sudo service tb-edge start
{:copy-code} {:copy-code}
``` ```
#### Step 8. Open ThingsBoard Edge UI #### Step 7. 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
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 #### Upgrading to ${TB_EDGE_VERSION}EDGE
**ThingsBoard Edge package download:** ThingsBoard Edge package download:
```bash ```bash
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} {:copy-code}
``` ```
##### ThingsBoard Edge service upgrade
Install package: Install package:
```bash ```bash
sudo rpm -Uvh tb-edge-${TB_EDGE_TAG}.rpm sudo rpm -Uvh tb-edge-${TB_EDGE_TAG}.rpm
{:copy-code} {:copy-code}
``` ```
${UPGRADE_DB} ${UPGRADE_DB}

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

@ -1,10 +1,3 @@
#### Upgrading to ${TB_EDGE_VERSION} #### 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} ${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 ```bash
nano docker-compose.yml nano docker-compose.yml
{:copy-code} {:copy-code}
``` ```
```text ```text
version: '3.8'
services: services:
mytbedge: mytbedge:
restart: always 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: Execute the following commands to up this docker compose directly:
```bash ```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 ```bash
> docker-compose-upgrade.yml && nano docker-compose-upgrade.yml > 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: Add the following lines to the yml file:
```bash ```bash
version: '3.8'
services: services:
mytbedge: mytbedge:
restart: on-failure restart: on-failure
@ -40,7 +39,7 @@ volumes:
{:copy-code} {:copy-code}
``` ```
Execute the following command to start upgrade process: Execute the following command to start an upgrade process:
```bash ```bash
docker compose -f docker-compose-upgrade.yml up --abort-on-container-exit 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 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: to stop and remove currently running TB Edge container:
@ -10,70 +10,6 @@ docker compose rm mytbedge
{:copy-code} {: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 ##### Backup Database
Make a copy of the database volume before upgrading: 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 ```bash
sudo systemctl tb-edge start 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 #### Upgrading to ${TB_EDGE_VERSION}EDGE
**ThingsBoard Edge package download:** ThingsBoard Edge package download:
```bash ```bash
wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.deb wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.deb
{:copy-code} {:copy-code}
``` ```
##### ThingsBoard Edge service upgrade
Install package: Install package:
```bash ```bash
sudo dpkg -i tb-edge-${TB_EDGE_TAG}.deb sudo dpkg -i tb-edge-${TB_EDGE_TAG}.deb
{:copy-code} {:copy-code}
``` ```
${UPGRADE_DB} ${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: Stop ThingsBoard Edge service:
@ -33,4 +33,4 @@ sudo -Hiu postgres pg_dump tb_edge > tb_edge.sql.bak
{:copy-code} {: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 jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; 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.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType; 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.Argument;
import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation; 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.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.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; 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.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; 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.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 org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry;
import java.util.ArrayList; import java.util.ArrayList;
@ -80,12 +69,15 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.DataConstants.SCOPE; 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; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto;
@TbRuleEngineComponent @TbRuleEngineComponent
@ -244,30 +236,17 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
private ListenableFuture<ArgumentEntry> fetchKvEntry(TenantId tenantId, EntityId entityId, Argument argument) { private ListenableFuture<ArgumentEntry> fetchKvEntry(TenantId tenantId, EntityId entityId, Argument argument) {
return switch (argument.getRefEntityKey().getType()) { return switch (argument.getRefEntityKey().getType()) {
case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument); case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument);
case ATTRIBUTE -> transformSingleValueArgument( case ATTRIBUTE -> Futures.transform(
Futures.transform( attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()),
attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()), result -> transformSingleValueArgument(result.orElseGet(() -> createDefaultAttributeEntry(argument, System.currentTimeMillis()))),
result -> result.or(() -> Optional.of(new BaseAttributeKvEntry(createDefaultKvEntry(argument), System.currentTimeMillis(), 0L))), calculatedFieldCallbackExecutor);
calculatedFieldCallbackExecutor) case TS_LATEST -> Futures.transform(
); timeseriesService.findLatest(tenantId, entityId, argument.getRefEntityKey().getKey()),
case TS_LATEST -> transformSingleValueArgument( result -> transformSingleValueArgument(result.orElseGet(() -> createDefaultTsKvEntry(argument, System.currentTimeMillis()))),
Futures.transform( calculatedFieldCallbackExecutor);
timeseriesService.findLatest(tenantId, entityId, argument.getRefEntityKey().getKey()),
result -> result.or(() -> Optional.of(new BasicTsKvEntry(System.currentTimeMillis(), createDefaultKvEntry(argument), 0L))),
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) { private ListenableFuture<ArgumentEntry> fetchTsRolling(TenantId tenantId, EntityId entityId, Argument argument) {
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow(); 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); 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 static class TbCallbackWrapper implements TbQueueCallback {
private final TbCallback callback; 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 @AllArgsConstructor
public abstract class BaseCalculatedFieldState implements CalculatedFieldState { public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
static final long DEFAULT_LAST_UPDATE_TS = -1L;
protected List<String> requiredArguments; protected List<String> requiredArguments;
protected Map<String, ArgumentEntry> arguments; protected Map<String, ArgumentEntry> arguments;
protected boolean sizeExceedsLimit; protected boolean sizeExceedsLimit;
protected long latestTimestamp = -1;
public BaseCalculatedFieldState(List<String> requiredArguments) { public BaseCalculatedFieldState(List<String> requiredArguments) {
this.requiredArguments = requiredArguments; this.requiredArguments = requiredArguments;
this.arguments = new HashMap<>(); this.arguments = new HashMap<>();
} }
public BaseCalculatedFieldState() { public BaseCalculatedFieldState() {
this(new ArrayList<>(), new HashMap<>(), false, -1); this(new ArrayList<>(), new HashMap<>(), false);
} }
@Override @Override
@ -73,7 +73,6 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
if (entryUpdated) { if (entryUpdated) {
stateUpdated = true; stateUpdated = true;
updateLastUpdateTimestamp(newEntry);
} }
} }
@ -109,15 +108,29 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
protected abstract void validateNewEntry(ArgumentEntry newEntry); protected abstract void validateNewEntry(ArgumentEntry newEntry);
private void updateLastUpdateTimestamp(ArgumentEntry entry) { public long getLatestTimestamp() {
long newTs = this.latestTimestamp; long latestTs = DEFAULT_LAST_UPDATE_TS;
if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) {
newTs = singleValueArgumentEntry.getTs(); boolean allDefault = arguments.values().stream().allMatch(entry -> {
} else if (entry instanceof TsRollingArgumentEntry tsRollingArgumentEntry) { if (entry instanceof SingleValueArgumentEntry single) {
Map.Entry<Long, Double> lastEntry = tsRollingArgumentEntry.getTsRecords().lastEntry(); return single.isDefaultValue();
newTs = (lastEntry != null) ? lastEntry.getKey() : System.currentTimeMillis(); }
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(); long latestTs = getLatestTimestamp();
if (useLatestTs && latestTs != -1) { if (useLatestTs && latestTs != DEFAULT_LAST_UPDATE_TS) {
ObjectNode resultNode = JacksonUtil.newObjectNode(); ObjectNode resultNode = JacksonUtil.newObjectNode();
resultNode.put("ts", latestTs); resultNode.put("ts", latestTs);
resultNode.set("values", valuesNode); 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 com.fasterxml.jackson.core.type.TypeReference;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; 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.AttributeValueProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS;
@Data @Data
@NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class SingleValueArgumentEntry implements ArgumentEntry { public class SingleValueArgumentEntry implements ArgumentEntry {
public static final Long DEFAULT_VERSION = -1L;
private long ts; private long ts;
private BasicKvEntry kvEntryValue; private BasicKvEntry kvEntryValue;
private Long version; private Long version;
private boolean forceResetPrevious; private boolean forceResetPrevious;
public SingleValueArgumentEntry() {
this.ts = DEFAULT_LAST_UPDATE_TS;
this.version = DEFAULT_VERSION;
}
public SingleValueArgumentEntry(TsKvProto entry) { public SingleValueArgumentEntry(TsKvProto entry) {
this.ts = entry.getTs(); this.ts = entry.getTs();
if (entry.hasVersion()) { if (entry.hasVersion()) {
@ -112,7 +119,7 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
@Override @Override
public boolean updateEntry(ArgumentEntry entry) { public boolean updateEntry(ArgumentEntry entry) {
if (entry instanceof SingleValueArgumentEntry singleValueEntry) { if (entry instanceof SingleValueArgumentEntry singleValueEntry) {
if (singleValueEntry.getTs() <= this.ts) { if (singleValueEntry.getTs() < this.ts) {
return false; return false;
} }
@ -128,4 +135,9 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
} }
return false; 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.Map;
import java.util.TreeMap; import java.util.TreeMap;
import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@ -83,6 +85,11 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
return tsRecords; return tsRecords;
} }
public long getLatestTs() {
var lastEntry = tsRecords.lastEntry();
return (lastEntry != null) ? lastEntry.getKey() : DEFAULT_LAST_UPDATE_TS;
}
@Override @Override
public TbelCfArg toTbelCfArg() { public TbelCfArg toTbelCfArg() {
List<TbelCfTsDoubleVal> values = new ArrayList<>(tsRecords.size()); 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.RequiredArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.server.service.install.InstallScripts; import org.thingsboard.server.service.install.InstallScripts;
import java.io.IOException; import java.io.IOException;
@ -26,6 +25,8 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import static org.thingsboard.edge.rpc.EdgeGrpcClient.getNewestEdgeVersion;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public abstract class BaseEdgeInstallUpgradeInstructionsService { public abstract class BaseEdgeInstallUpgradeInstructionsService {
@ -35,9 +36,12 @@ public abstract class BaseEdgeInstallUpgradeInstructionsService {
private final InstallScripts installScripts; private final InstallScripts installScripts;
@Value("${app.version:unknown}")
@Setter @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) { protected String readFile(Path file) {
try { 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("${EXTRA_HOSTS}", "");
dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", baseUrl); dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", baseUrl);
} }
String edgeVersion = appVersion + "EDGE"; dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", platformEdgeVersion + "EDGE");
edgeVersion = edgeVersion.replace("-SNAPSHOT", "");
dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion);
dockerInstallInstructions = replacePlaceholders(dockerInstallInstructions, edge); dockerInstallInstructions = replacePlaceholders(dockerInstallInstructions, edge);
return new EdgeInstructions(dockerInstallInstructions); return new EdgeInstructions(dockerInstallInstructions);
} }
@ -76,9 +74,8 @@ public class DefaultEdgeInstallInstructionsService extends BaseEdgeInstallUpgrad
String ubuntuInstallInstructions = readFile(resolveFile(os, "instructions.md")); String ubuntuInstallInstructions = readFile(resolveFile(os, "instructions.md"));
ubuntuInstallInstructions = replacePlaceholders(ubuntuInstallInstructions, edge); ubuntuInstallInstructions = replacePlaceholders(ubuntuInstallInstructions, edge);
ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${BASE_URL}", request.getServerName()); ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${BASE_URL}", request.getServerName());
String edgeVersion = appVersion.replace("-SNAPSHOT", ""); ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_VERSION}", platformEdgeVersion);
ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_TAG}", getTagVersion(platformEdgeVersion));
ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_TAG}", getTagVersion(edgeVersion));
return new EdgeInstructions(ubuntuInstallInstructions); 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 @Override
public EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod) { public EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod) {
String tbVersion = appVersion.replace("-SNAPSHOT", "");
String currentEdgeVersion = convertEdgeVersionToDocsFormat(edgeVersion); String currentEdgeVersion = convertEdgeVersionToDocsFormat(edgeVersion);
return switch (upgradeMethod.toLowerCase()) { return switch (upgradeMethod.toLowerCase()) {
case "docker" -> getDockerUpgradeInstructions(tbVersion, currentEdgeVersion); case "docker" -> getDockerUpgradeInstructions(this.platformEdgeVersion, currentEdgeVersion);
case "ubuntu", "centos" -> case "ubuntu", "centos" ->
getLinuxUpgradeInstructions(tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase()); getLinuxUpgradeInstructions(this.platformEdgeVersion, currentEdgeVersion, upgradeMethod.toLowerCase());
default -> throw new IllegalArgumentException("Unsupported upgrade method for Edge: " + upgradeMethod); 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(); Optional<AttributeKvEntry> attributeKvEntryOpt = attributesService.find(tenantId, edgeId, AttributeScope.SERVER_SCOPE, DataConstants.EDGE_VERSION_ATTR_KEY).get();
if (attributeKvEntryOpt.isPresent()) { if (attributeKvEntryOpt.isPresent()) {
String edgeVersionFormatted = convertEdgeVersionToDocsFormat(attributeKvEntryOpt.get().getValueAsString()); String edgeVersionFormatted = convertEdgeVersionToDocsFormat(attributeKvEntryOpt.get().getValueAsString());
String appVersionFormatted = appVersion.replace("-SNAPSHOT", ""); return isVersionGreaterOrEqualsThan(edgeVersionFormatted, "3.6.0") && !isVersionGreaterOrEqualsThan(edgeVersionFormatted, platformEdgeVersion);
return isVersionGreaterOrEqualsThan(edgeVersionFormatted, "3.6.0") && !isVersionGreaterOrEqualsThan(edgeVersionFormatted, appVersionFormatted);
} }
return false; return false;
} }
@ -98,13 +96,13 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad
return true; return true;
} }
private EdgeInstructions getDockerUpgradeInstructions(String tbVersion, String currentEdgeVersion) { private EdgeInstructions getDockerUpgradeInstructions(String platformEdgeVersion, String currentEdgeVersion) {
EdgeUpgradeInfo edgeUpgradeInfo = upgradeVersionHashMap.get(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."); return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available.");
} }
StringBuilder result = new StringBuilder(readFile(resolveFile("docker", "upgrade_preparing.md"))); 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 edgeVersion = edgeUpgradeInfo.getNextEdgeVersion();
String dockerUpgradeInstructions = readFile(resolveFile("docker", "instructions.md")); String dockerUpgradeInstructions = readFile(resolveFile("docker", "instructions.md"));
if (edgeUpgradeInfo.isRequiresUpdateDb()) { if (edgeUpgradeInfo.isRequiresUpdateDb()) {
@ -125,15 +123,15 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad
return new EdgeInstructions(result.toString()); 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); 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."); return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available.");
} }
String upgrade_preparing = readFile(resolveFile("upgrade_preparing.md")); String upgrade_preparing = readFile(resolveFile("upgrade_preparing.md"));
upgrade_preparing = upgrade_preparing.replace("${OS}", os.equals("centos") ? "RHEL/CentOS 7/8" : "Ubuntu"); upgrade_preparing = upgrade_preparing.replace("${OS}", os.equals("centos") ? "RHEL/CentOS 7/8" : "Ubuntu");
StringBuilder result = new StringBuilder(upgrade_preparing); 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 edgeVersion = edgeUpgradeInfo.getNextEdgeVersion();
String linuxUpgradeInstructions = readFile(resolveFile(os, "instructions.md")); String linuxUpgradeInstructions = readFile(resolveFile(os, "instructions.md"));
if (edgeUpgradeInfo.isRequiresUpdateDb()) { if (edgeUpgradeInfo.isRequiresUpdateDb()) {
@ -155,10 +153,6 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad
return new EdgeInstructions(result.toString()); return new EdgeInstructions(result.toString());
} }
private String convertEdgeVersionToDocsFormat(String edgeVersion) {
return edgeVersion.replace("_", ".").substring(2);
}
@Override @Override
protected String getBaseDirName() { protected String getBaseDirName() {
return UPGRADE_DIR; 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); 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 updateInstructionMap(Map<String, EdgeUpgradeInfo> upgradeVersions);
void setAppVersion(String version); void setPlatformEdgeVersion(String version);
boolean isUpgradeAvailable(TenantId tenantId, EdgeId edgeId) throws Exception; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -82,6 +83,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer; 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.ACTIVITY_STATE;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_CONNECT_TIME; 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 { public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService {
private final ConcurrentMap<EdgeId, EdgeGrpcSession> sessions = new ConcurrentHashMap<>(); 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 ConcurrentMap<EdgeId, Lock> sessionNewEventsLocks = new ConcurrentHashMap<>();
private final Map<EdgeId, Boolean> sessionNewEvents = new HashMap<>(); private final Map<EdgeId, Boolean> sessionNewEvents = new HashMap<>();
private final ConcurrentMap<EdgeId, ScheduledFuture<?>> sessionEdgeEventChecks = new ConcurrentHashMap<>(); private final ConcurrentMap<EdgeId, ScheduledFuture<?>> sessionEdgeEventChecks = new ConcurrentHashMap<>();
@ -283,6 +286,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
destroySession(session); destroySession(session);
session.cleanUp(); session.cleanUp();
sessions.remove(edgeId); sessions.remove(edgeId);
sessionsById.remove(session.getSessionId());
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock(); newEventLock.lock();
try { try {
@ -332,9 +336,15 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
TenantId tenantId = edge.getTenantId(); TenantId tenantId = edge.getTenantId();
log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId); log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId);
if (sessions.containsKey(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); sessions.put(edgeId, edgeGrpcSession);
sessionsById.put(edgeGrpcSession.getSessionId(), edgeGrpcSession);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock(); newEventLock.lock();
try { try {
@ -492,9 +502,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void onEdgeDisconnect(Edge edge, UUID sessionId) { private void onEdgeDisconnect(Edge edge, UUID sessionId) {
EdgeId edgeId = edge.getId(); EdgeId edgeId = edge.getId();
log.info("[{}][{}] edge disconnected!", edgeId, sessionId); log.info("[{}][{}] edge disconnected!", edgeId, sessionId);
EdgeGrpcSession toRemove = sessions.get(edgeId); EdgeGrpcSession current = sessions.get(edgeId);
if (toRemove.getSessionId().equals(sessionId)) { if (current != null && current.getSessionId().equals(sessionId)) {
toRemove = sessions.remove(edgeId); EdgeGrpcSession toRemove = sessions.remove(edgeId);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock(); newEventLock.lock();
try { try {
@ -503,6 +513,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
newEventLock.unlock(); newEventLock.unlock();
} }
destroySession(toRemove); destroySession(toRemove);
sessionsById.remove(sessionId);
TenantId tenantId = toRemove.getEdge().getTenantId(); TenantId tenantId = toRemove.getEdge().getTenantId();
save(tenantId, edgeId, ACTIVITY_STATE, false); save(tenantId, edgeId, ACTIVITY_STATE, false);
long lastDisconnectTs = System.currentTimeMillis(); long lastDisconnectTs = System.currentTimeMillis();
@ -510,7 +521,18 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edge, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT); pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edge, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT);
cancelScheduleEdgeEventsCheck(edgeId); cancelScheduleEdgeEventsCheck(edgeId);
} else { } 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); edgeIdServiceIdCache.evict(edgeId);
} }
@ -522,6 +544,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId()); session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId());
zombieSessions.add(session); 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() { private void cleanupZombieSessions() {
try { try {
List<EdgeId> toRemove = new ArrayList<>(); tryToDestroyZombieSessions(getZombieSessions(sessions.values()), s -> sessions.remove(s.getEdge().getId()));
for (EdgeGrpcSession session : sessions.values()) { tryToDestroyZombieSessions(getZombieSessions(sessionsById.values()), s -> sessionsById.remove(s.getSessionId()));
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);
}
}
}
zombieSessions.removeIf(zombie -> { zombieSessions.removeIf(zombie -> {
if (zombie.destroy()) { if (zombie.destroy()) {
log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].", 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 @Override
public ListenableFuture<Boolean> processEdgeEvents() { 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())) { if (consumer == null || (consumer.getConsumer() != null && consumer.getConsumer().isStopped())) {
try { 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.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer"));
this.consumer = QueueConsumerManager.<TbProtoQueueMsg<ToEdgeEventNotificationMsg>>builder() this.consumer = QueueConsumerManager.<TbProtoQueueMsg<ToEdgeEventNotificationMsg>>builder()
.name("TB Edge events [" + edge.getId() + "]") .name("TB Edge events [" + edge.getId() + "]")
@ -133,6 +146,7 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
public boolean destroy() { public boolean destroy() {
try { try {
if (consumer != null) { if (consumer != null) {
log.info("[{}][{}] Stopping edge event consumer...", tenantId, edge != null ? edge.getId() : null);
consumer.stop(); consumer.stop();
} }
} catch (Exception e) { } catch (Exception e) {
@ -141,16 +155,25 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
} }
consumer = null; consumer = null;
try { try {
if (consumerExecutor != null) { if (consumerExecutor != null && !consumerExecutor.isShutdown()) {
consumerExecutor.shutdown(); consumerExecutor.shutdown();
awaitConsumerTermination();
} }
} catch (Exception e) { } 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 false;
} }
return true; 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 @Override
public void cleanUp() { public void cleanUp() {
String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edge.getId()).getTopic(); 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); Dashboard savedDashboard = edgeCtx.getDashboardService().saveDashboard(dashboard, false);
updateDashboardAssignments(tenantId, dashboardById, savedDashboard, newAssignedCustomers); updateDashboardAssignments(tenantId, customerId, dashboardById, savedDashboard, newAssignedCustomers);
return created; 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<>(); Set<ShortCustomerInfo> currentAssignedCustomers = new HashSet<>();
if (dashboardById != null) { if (dashboardById != null) {
if (dashboardById.getAssignedCustomers() != 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> addedCustomerIds = new HashSet<>();
Set<CustomerId> removedCustomerIds = 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.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventType; 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.DashboardId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType; 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.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@Component @Component
@ -127,14 +130,24 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor implements Da
} }
@Override @Override
protected Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers) { protected Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, CustomerId edgeCustomerId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers) {
newAssignedCustomers.addAll(currentAssignedCustomers); boolean edgeCustomerPresentInNewAssignments = newAssignedCustomers.stream()
return newAssignedCustomers; .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 @Override
public EdgeEventType getEdgeEventType() { public EdgeEventType getEdgeEventType() {
return EdgeEventType.DASHBOARD; 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) { private void handleAlarmStatusSubscriptionUpdate(TbSubscription<AlarmSubscriptionUpdate> sub, AlarmSubscriptionUpdate subscriptionUpdate) {
try { try {
AlarmInfo alarm = subscriptionUpdate.getAlarm(); AlarmInfo alarm = subscriptionUpdate.getAlarm();
if (!alarm.getOriginator().equals(subscription.getEntityId())) {
return;
}
Set<UUID> alarmsIds = subscription.getAlarmIds(); Set<UUID> alarmsIds = subscription.getAlarmIds();
if (alarmsIds.contains(alarm.getId().getId())) { if (alarmsIds.contains(alarm.getId().getId())) {
if (!subscription.matches(alarm) || subscriptionUpdate.isAlarmDeleted()) { 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()); .build());
} }
ObjectNode edgeRequest = JacksonUtil.newObjectNode().put(VERSION_PARAM, version); 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); String edgePlatformVersion = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/installMapping", new HttpEntity<>(edgeRequest.toString(), headers), String.class);
if (edgeInstallVersion != null) { if (edgePlatformVersion != null) {
edgeInstallInstructionsService.setAppVersion(edgeInstallVersion); edgeInstallInstructionsService.setPlatformEdgeVersion(edgePlatformVersion);
edgeUpgradeInstructionsService.setAppVersion(edgeInstallVersion); edgeUpgradeInstructionsService.setPlatformEdgeVersion(edgePlatformVersion);
} }
EdgeUpgradeMessage edgeUpgradeMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/upgradeMapping", new HttpEntity<>(edgeRequest.toString(), headers), EdgeUpgradeMessage.class); EdgeUpgradeMessage edgeUpgradeMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/upgradeMapping", new HttpEntity<>(edgeRequest.toString(), headers), EdgeUpgradeMessage.class);
if (edgeUpgradeMessage != null) { 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.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest @DaoSqlTest
public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest { public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest {
@ -570,6 +571,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
@Test @Test
public void testScriptCalculatedFieldWhenUsedLatestTsInScript() throws Exception { public void testScriptCalculatedFieldWhenUsedLatestTsInScript() throws Exception {
Device testDevice = createDevice("Test device", "1234567890"); Device testDevice = createDevice("Test device", "1234567890");
long ts = System.currentTimeMillis() - 300000L; 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))); 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 @Test
public void testSimpleCalculatedFieldWhenCtxBecameUninitialized() throws Exception { public void testSimpleCalculatedFieldWhenCtxBecameUninitialized() throws Exception {
Device testDevice = createDevice("Test device", "1234567890"); 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 @Test
public void testGetEdgeUpgradeInstructions() throws Exception { 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<>(); HashMap<String, EdgeUpgradeInfo> upgradeInfoHashMap = new HashMap<>();
upgradeInfoHashMap.put("3.6.0", new EdgeUpgradeInfo(true, "3.6.1")); upgradeInfoHashMap.put("3.6.0", new EdgeUpgradeInfo(true, "3.6.1"));
upgradeInfoHashMap.put("3.6.1", new EdgeUpgradeInfo(true, "3.6.2")); 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 // Test 3.5.0 Edge - upgrade not available
String body = "{\"edgeVersion\": \"V_3_5_0\"}"; String body = "{\"edgeVersion\": \"V_3_5_0\"}";
doPostAsync("/api/plugins/telemetry/EDGE/" + savedEdge.getId().getId() + "/attributes/SERVER_SCOPE", body, String.class, status().isOk()); 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())); 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())); 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())); Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
// Test 3.6.0 Edge - upgrade available // Test 3.6.0 Edge - upgrade available
body = "{\"edgeVersion\": \"V_3_6_0\"}"; body = "{\"edgeVersion\": \"V_3_6_0\"}";
doPostAsync("/api/plugins/telemetry/EDGE/" + savedEdge.getId().getId() + "/attributes/SERVER_SCOPE", body, String.class, status().isOk()); 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())); 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())); Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.1.6-SNAPSHOT"); edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.2");
Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.2");
Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
// Test 3.6.1 Edge - upgrade available // Test 3.6.1 Edge - upgrade available
body = "{\"edgeVersion\": \"V_3_6_1\"}"; body = "{\"edgeVersion\": \"V_3_6_1\"}";
doPostAsync("/api/plugins/telemetry/EDGE/" + savedEdge.getId().getId() + "/attributes/SERVER_SCOPE", body, String.class, status().isOk()); 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())); Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.2"); edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.2");
Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.2-SNAPSHOT");
Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); 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())); 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.Device;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; 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.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; 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.NumericFilterPredicate;
import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.data.query.TsValue; 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.dao.service.DaoSqlTest;
import org.thingsboard.server.service.subscription.SubscriptionErrorCode; import org.thingsboard.server.service.subscription.SubscriptionErrorCode;
import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope;
@ -478,6 +480,56 @@ public class WebsocketApiTest extends AbstractControllerTest {
Assert.assertFalse(alarmStatusUpdate5.isActive()); 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 @Test
public void testAlarmStatusWsCmdWithMaxAlarmsCacheSize() throws Exception { public void testAlarmStatusWsCmdWithMaxAlarmsCacheSize() throws Exception {
loginTenantAdmin(); 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.ShortCustomerInfo;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.Edge; 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.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest; 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.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EdgeConfiguration;
import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg;
@ -182,6 +186,22 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
customer.setTitle("Edge Customer"); customer.setTitle("Edge Customer");
Customer savedCustomer = doPost("/api/customer", customer, Customer.class); 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); Dashboard dashboard = buildDashboardForUplinkMsg(savedCustomer);
// create dashboard on edge // create dashboard on edge
@ -224,6 +244,23 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
foundDashboard = doGet("/api/dashboard/" + dashboard.getUuidId(), Dashboard.class); foundDashboard = doGet("/api/dashboard/" + dashboard.getUuidId(), Dashboard.class);
Assert.assertEquals(DASHBOARD_TITLE + " Updated", foundDashboard.getName()); 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 @Test

2
common/data/pom.xml

@ -113,7 +113,7 @@
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId> <artifactId>langchain4j-core</artifactId>
</dependency> </dependency>
</dependencies> </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.id.TenantId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoNullChar; import org.thingsboard.server.common.data.validation.NoNullChar;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial; import java.io.Serial;
@ -64,6 +65,7 @@ public final class AiModel extends BaseData<AiModelId> implements HasTenantId, H
@NotBlank @NotBlank
@NoNullChar @NoNullChar
@Length(min = 1, max = 255) @Length(min = 1, max = 255)
@NoXss
@Schema( @Schema(
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE, 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.OAuth2ClientId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.util.List; import java.util.List;
@ -42,6 +43,7 @@ public class OAuth2Client extends BaseDataWithAdditionalInfo<OAuth2ClientId> imp
private TenantId tenantId; private TenantId tenantId;
@Schema(description = "Oauth2 client title") @Schema(description = "Oauth2 client title")
@NotBlank @NotBlank
@NoXss
@Length(fieldName = "title", max = 100, message = "cannot be longer than 100 chars") @Length(fieldName = "title", max = 100, message = "cannot be longer than 100 chars")
private String title; private String title;
@Schema(description = "Config for mapping OAuth2 log in response to platform entities", requiredMode = Schema.RequiredMode.REQUIRED) @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() .setConnectRequestMsg(ConnectRequestMsg.newBuilder()
.setEdgeRoutingKey(edgeKey) .setEdgeRoutingKey(edgeKey)
.setEdgeSecret(edgeSecret) .setEdgeSecret(edgeSecret)
.setEdgeVersion(EdgeVersion.V_4_2_0) .setEdgeVersion(getNewestEdgeVersion())
.setMaxInboundMessageSize(maxInboundMessageSize) .setMaxInboundMessageSize(maxInboundMessageSize)
.build()) .build())
.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, private StreamObserver<ResponseMsg> initOutputStream(String edgeKey,
Consumer<UplinkResponseMsg> onUplinkResponse, Consumer<UplinkResponseMsg> onUplinkResponse,
Consumer<EdgeConfiguration> onEdgeUpdate, Consumer<EdgeConfiguration> onEdgeUpdate,

4
common/queue/pom.xml

@ -68,6 +68,10 @@
<groupId>org.apache.kafka</groupId> <groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId> <artifactId>kafka-clients</artifactId>
</dependency> </dependency>
<dependency>
<groupId>at.yawk.lz4</groupId>
<artifactId>lz4-java</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.google.cloud</groupId> <groupId>com.google.cloud</groupId>
<artifactId>google-cloud-pubsub</artifactId> <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 CLAIMING = "claiming";
public static final String ATTRIBUTE = "attribute"; public static final String ATTRIBUTE = "attribute";
public static final String RPC_RESPONSE = "Rpc response"; public static final String RPC_RESPONSE = "Rpc response";
public static final String CONNECT = "connect";
public static final String ATTRIBUTES_REQUEST = "attributes request"; public static final String ATTRIBUTES_REQUEST = "attributes request";
protected final MqttTransportContext context; protected final MqttTransportContext context;
@ -257,12 +258,14 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
protected void processOnConnect(MqttPublishMessage msg, String deviceName, String deviceType) { protected void processOnConnect(MqttPublishMessage msg, String deviceName, String deviceType) {
log.trace("[{}][{}][{}] onDeviceConnect: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName); log.trace("[{}][{}][{}] onDeviceConnect: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName);
int msgId = getMsgId(msg);
AtomicBoolean ackSent = new AtomicBoolean(false);
process(onDeviceConnect(deviceName, deviceType), process(onDeviceConnect(deviceName, deviceType),
result -> { result -> {
ack(msg, MqttReasonCodes.PubAck.SUCCESS); ack(msg, MqttReasonCodes.PubAck.SUCCESS);
log.trace("[{}][{}][{}] onDeviceConnectOk: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName); 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) { 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 @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class KeyDictionaryCompositeKey implements Serializable{ public class KeyDictionaryCompositeKey implements Serializable {
@Transient @Transient
private static final long serialVersionUID = -4089175869616037523L; 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) @Column(name = KEY_COLUMN)
private String key; private String key;
@Column(name = KEY_ID_COLUMN, unique = true, columnDefinition = "int") @Column(name = KEY_ID_COLUMN, unique = true, columnDefinition = "int", insertable = false, updatable = false)
@Generated private Integer keyId;
private int 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -48,43 +46,34 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService
@Transactional(propagation = Propagation.NOT_SUPPORTED) @Transactional(propagation = Propagation.NOT_SUPPORTED)
@Override @Override
public Integer getOrSaveKeyId(String strKey) { public Integer getOrSaveKeyId(String strKey) {
Integer keyId = keyDictionaryMap.get(strKey); Integer cached = keyDictionaryMap.get(strKey);
if (keyId == null) { if (cached != null) {
Optional<KeyDictionaryEntry> tsKvDictionaryOptional; return cached;
tsKvDictionaryOptional = keyDictionaryRepository.findById(new KeyDictionaryCompositeKey(strKey)); }
if (tsKvDictionaryOptional.isEmpty()) { var compositeKey = new KeyDictionaryCompositeKey(strKey);
creationLock.lock(); Optional<Integer> existingId = keyDictionaryRepository.findById(compositeKey).map(KeyDictionaryEntry::getKeyId);
try { if (existingId.isPresent()) {
keyId = keyDictionaryMap.get(strKey); return cacheAndReturn(strKey, existingId.get());
if (keyId != null) { }
return keyId; creationLock.lock();
} try {
tsKvDictionaryOptional = keyDictionaryRepository.findById(new KeyDictionaryCompositeKey(strKey)); Integer fromCache = keyDictionaryMap.get(strKey);
if (tsKvDictionaryOptional.isEmpty()) { if (fromCache != null) {
KeyDictionaryEntry keyDictionaryEntry = new KeyDictionaryEntry(); return fromCache;
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 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 @Override
@ -98,4 +87,9 @@ public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService
return DaoUtil.pageToPageData(keyDictionaryRepository.findAll(DaoUtil.toPageable(pageLink))); 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.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; 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.KeyDictionaryCompositeKey;
import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry; 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") @Query("SELECT e FROM KeyDictionaryEntry e ORDER BY e.keyId ASC")
Page<KeyDictionaryEntry> findAll(Pageable pageable); 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": { "dependencies": {
"config": "^4.1.1", "config": "^4.1.1",
"express": "^5.1.0", "express": "^5.1.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.1",
"kafkajs": "^2.2.4", "kafkajs": "^2.2.4",
"long": "^5.3.2", "long": "^5.3.2",
"uuid-parse": "^1.1.0", "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" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
js-yaml@^4.1.0: js-yaml@^4.1.1:
version "4.1.0" version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
dependencies: dependencies:
argparse "^2.0.1" argparse "^2.0.1"
@ -1533,9 +1533,9 @@ supports-preserve-symlinks-flag@^1.0.0:
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tar-fs@^2.0.0, tar-fs@^2.1.1: tar-fs@^2.0.0, tar-fs@^2.1.1:
version "2.1.3" version "2.1.4"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.3.tgz#fb3b8843a26b6f13a08e606f7922875eb1fbbf92" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930"
integrity sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg== integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==
dependencies: dependencies:
chownr "^1.1.1" chownr "^1.1.1"
mkdirp-classic "^0.5.2" mkdirp-classic "^0.5.2"

2
msa/web-ui/package.json

@ -19,7 +19,7 @@
"express": "^5.1.0", "express": "^5.1.0",
"http": "0.0.0", "http": "0.0.0",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.1",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.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); logger.debug(req.method + ' ' + req.originalUrl);
apiProxy.web(req, res); apiProxy.web(req, res);
}); });
app.all('/static/rulenode/*', (req, res) => { app.all('/static/rulenode/*splat', (req, res) => {
apiProxy.web(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" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
js-yaml@^4.1.0: js-yaml@^4.1.1:
version "4.1.0" version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
dependencies: dependencies:
argparse "^2.0.1" argparse "^2.0.1"
@ -1615,9 +1615,9 @@ supports-preserve-symlinks-flag@^1.0.0:
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tar-fs@^2.0.0, tar-fs@^2.1.1: tar-fs@^2.0.0, tar-fs@^2.1.1:
version "2.1.3" version "2.1.4"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.3.tgz#fb3b8843a26b6f13a08e606f7922875eb1fbbf92" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930"
integrity sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg== integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==
dependencies: dependencies:
chownr "^1.1.1" chownr "^1.1.1"
mkdirp-classic "^0.5.2" mkdirp-classic "^0.5.2"

32
pom.xml

@ -63,7 +63,7 @@
<curator.version>5.6.0</curator.version> <curator.version>5.6.0</curator.version>
<zookeeper.version>3.9.3</zookeeper.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--> <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> <tbel.version>1.2.8</tbel.version>
<lombok.version>1.18.38</lombok.version> <lombok.version>1.18.38</lombok.version>
<paho.client.version>1.2.5</paho.client.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. 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 --> 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> <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> <bucket4j.version>8.10.1</bucket4j.version>
<antlr.version>3.5.3</antlr.version> <antlr.version>3.5.3</antlr.version>
<snakeyaml.version>2.2</snakeyaml.version> <snakeyaml.version>2.2</snakeyaml.version>
@ -101,7 +102,7 @@
<passay.version>1.6.4</passay.version> <passay.version>1.6.4</passay.version>
<ua-parser.version>1.6.1</ua-parser.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> <commons-collections.version>4.4</commons-collections.version>
<protobuf-dynamic.version>1.0.4TB</protobuf-dynamic.version> <protobuf-dynamic.version>1.0.4TB</protobuf-dynamic.version>
<wire-schema.version>3.7.1</wire-schema.version> <wire-schema.version>3.7.1</wire-schema.version>
@ -110,7 +111,7 @@
<jakarta.el.version>4.0.2</jakarta.el.version> <jakarta.el.version>4.0.2</jakarta.el.version>
<antisamy.version>1.7.5</antisamy.version> <antisamy.version>1.7.5</antisamy.version>
<snmp4j.version>3.8.0</snmp4j.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> <error_prone_annotations.version>2.38.0</error_prone_annotations.version>
<animal-sniffer-annotations.version>1.24</animal-sniffer-annotations.version> <animal-sniffer-annotations.version>1.24</animal-sniffer-annotations.version>
<auto-value-annotations.version>1.11.0</auto-value-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> <firebase-admin.version>9.2.0</firebase-admin.version>
<snappy.version>1.1.10.5</snappy.version> <snappy.version>1.1.10.5</snappy.version>
<rocksdbjni.version>9.10.0</rocksdbjni.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> </properties>
<modules> <modules>
@ -911,7 +912,7 @@
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId> <artifactId>langchain4j-bom</artifactId>
<version>${langchain4j.version}</version> <version>${langchain4j.version}</version>
<type>pom</type> <type>pom</type>
@ -1153,6 +1154,17 @@
<groupId>org.apache.kafka</groupId> <groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId> <artifactId>kafka-clients</artifactId>
<version>${kafka.version}</version> <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>
<dependency> <dependency>
<groupId>com.github.springtestdbunit</groupId> <groupId>com.github.springtestdbunit</groupId>
@ -1433,6 +1445,12 @@
<groupId>org.apache.cassandra</groupId> <groupId>org.apache.cassandra</groupId>
<artifactId>cassandra-all</artifactId> <artifactId>cassandra-all</artifactId>
<version>${cassandra-all.version}</version> <version>${cassandra-all.version}</version>
<exclusions>
<exclusion>
<groupId>org.lz4</groupId>
<artifactId>lz4-java</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.testng</groupId> <groupId>org.testng</groupId>
@ -1908,6 +1926,10 @@
<id>central</id> <id>central</id>
<url>https://repo1.maven.org/maven2/</url> <url>https://repo1.maven.org/maven2/</url>
</repository> </repository>
<repository>
<id>thingsboard-repo</id>
<url>https://repo.thingsboard.io/artifactory/libs-release-public</url>
</repository>
<repository> <repository>
<id>spring-snapshots</id> <id>spring-snapshots</id>
<name>Spring Snapshots</name> <name>Spring Snapshots</name>

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

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

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

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

4
tools/pom.xml

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

5
ui-ngx/package.json

@ -141,6 +141,9 @@
"rollup": "4.22.4", "rollup": "4.22.4",
"@babel/core": "7.25.2", "@babel/core": "7.25.2",
"esbuild": "0.23.0", "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: { chatModelConfig: {
modelType: ModelType.CHAT, modelType: ModelType.CHAT,
provider: this.data.AIModel.configuration.provider, ...this.data.AIModel.configuration,
providerConfig: {...this.data.AIModel.configuration.providerConfig},
modelId: this.data.AIModel.configuration.modelId,
maxRetries: 0, maxRetries: 0,
timeoutSeconds: 20 timeoutSeconds: 20
} }

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

@ -14,7 +14,7 @@
/// limitations under the License. /// 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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -51,7 +51,7 @@ export interface AlarmDetailsDialogData {
templateUrl: './alarm-details-dialog.component.html', templateUrl: './alarm-details-dialog.component.html',
styleUrls: ['./alarm-details-dialog.component.scss'] styleUrls: ['./alarm-details-dialog.component.scss']
}) })
export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDialogComponent, boolean> implements OnInit { export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDialogComponent, boolean> {
alarmId: string; alarmId: string;
alarmFormGroup: UntypedFormGroup; alarmFormGroup: UntypedFormGroup;
@ -128,13 +128,12 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
this.alarmFormGroup.get('startTime') this.alarmFormGroup.get('startTime')
.patchValue(this.datePipe.transform(alarm.startTs, 'yyyy-MM-dd HH:mm:ss')); .patchValue(this.datePipe.transform(alarm.startTs, 'yyyy-MM-dd HH:mm:ss'));
} }
if (alarm.startTs || alarm.endTs) { if (alarm.startTs || alarm.clearTs) {
let duration = ''; 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); duration = this.millisecondsToTimeStringPipe.transform(Date.now() - alarm.startTs);
} } else if (alarm.clearTs && alarm.cleared) {
if (alarm.endTs && (alarm.status === AlarmStatus.CLEARED_ACK || alarm.status === AlarmStatus.CLEARED_UNACK)) { duration = this.millisecondsToTimeStringPipe.transform(alarm.clearTs - alarm.startTs);
duration = this.millisecondsToTimeStringPipe.transform(alarm.endTs - alarm.startTs);
} }
this.alarmFormGroup.get('duration').patchValue(duration); this.alarmFormGroup.get('duration').patchValue(duration);
} }
@ -144,9 +143,6 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
this.alarmFormGroup.get('alarmDetails').patchValue(alarm.details); this.alarmFormGroup.get('alarmDetails').patchValue(alarm.details);
} }
ngOnInit(): void {
}
close(): void { close(): void {
this.dialogRef.close(this.alarmUpdated); 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; this.table = null;
} }
updateData(closeDetails = false) { updateData(closeDetails = false, reloadEntity = true) {
if (this.table) { if (this.table) {
this.table.updateData(closeDetails); this.table.updateData(closeDetails, reloadEntity);
} else if (this.entityDetailsPage) { } else if (this.entityDetailsPage) {
this.entityDetailsPage.reload(); 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; addEnabled(): boolean;
clearSelection(): void; clearSelection(): void;
updateData(closeDetails?: boolean): void; updateData(closeDetails?: boolean, reloadEntity?: boolean): void;
onRowClick($event: Event, entity): void; onRowClick($event: Event, entity): void;
toggleEntityDetails($event: Event, entity); toggleEntityDetails($event: Event, entity);
addEntity($event: Event): void; 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() { 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() { onDeviceProfileUpdated() {
this.entitiesTableConfig.updateData(false); this.entitiesTableConfig.updateData(false, false);
} }
onDeviceProfileChanged(deviceProfile: DeviceProfileInfo) { 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-group [(selectedIndex)]="tabIndex" (selectedTabChange)="selectedTabChange(tabIndex)">
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="mdi:ubuntu"></mat-icon> <mat-icon class="tabs-icon" svgIcon="docker"></mat-icon>
Ubuntu Docker
</ng-template> </ng-template>
<ng-template matTabContent> <ng-template matTabContent>
<div class="tb-form-panel no-padding no-border"> <div class="tb-form-panel no-padding no-border">
<div class="tb-form-panel no-padding stroked"> <div class="tb-form-panel no-padding stroked">
<tb-markdown [data]='contentData.ubuntu'></tb-markdown> <tb-markdown [data]='contentData.docker'></tb-markdown>
</div> </div>
</div> </div>
</ng-template> </ng-template>
</mat-tab> </mat-tab>
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="mdi:centos"></mat-icon> <mat-icon class="tabs-icon" svgIcon="mdi:ubuntu"></mat-icon>
CentOS/RHEL Ubuntu
</ng-template> </ng-template>
<ng-template matTabContent> <ng-template matTabContent>
<div class="tb-form-panel no-padding no-border"> <div class="tb-form-panel no-padding no-border">
<div class="tb-form-panel no-padding stroked"> <div class="tb-form-panel no-padding stroked">
<tb-markdown [data]='contentData.centos'></tb-markdown> <tb-markdown [data]='contentData.ubuntu'></tb-markdown>
</div> </div>
</div> </div>
</ng-template> </ng-template>
</mat-tab> </mat-tab>
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="docker"></mat-icon> <mat-icon class="tabs-icon" svgIcon="mdi:centos"></mat-icon>
Docker CentOS/RHEL
</ng-template> </ng-template>
<ng-template matTabContent> <ng-template matTabContent>
<div class="tb-form-panel no-padding no-border"> <div class="tb-form-panel no-padding no-border">
<div class="tb-form-panel no-padding stroked"> <div class="tb-form-panel no-padding stroked">
<tb-markdown [data]='contentData.docker'></tb-markdown> <tb-markdown [data]='contentData.centos'></tb-markdown>
</div> </div>
</div> </div>
</ng-template> </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, context: ctx,
showCloseButton: false, showCloseButton: false,
popoverContentStyle: {padding: '16px 24px'}, popoverContentStyle: {padding: '16px 24px'},
isModal: false isModal: true
}); });
releaseNotesPanelPopover.tbComponentRef.instance.popover = releaseNotesPanelPopover; releaseNotesPanelPopover.tbComponentRef.instance.popover = releaseNotesPanelPopover;
releaseNotesPanelPopover.tbComponentRef.instance.editorContentApplied.subscribe((releaseNotes) => { 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, resize: false,
setup: (editor) => { setup: (editor) => {
editor.on('PostRender', function() { editor.on('PostRender', function() {
const container = editor.getContainer().closest('.tb-popover-content'); const container = document.querySelector('.tox.tox-tinymce-aux');
const uiContainer = document.querySelector('.tox.tox-tinymce-aux'); const styleSheet = document.createElement('style');
container.parentNode.appendChild(uiContainer); 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, 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"> <ng-container *ngIf="targetNotificationForm.get('configuration.usersFilter.filterByTenants').value; else tenantProfiles">
<tb-entity-list <tb-entity-list
formControlName="tenantsIds" formControlName="tenantsIds"
syncIdsWithDB
subscriptSizing="dynamic" subscriptSizing="dynamic"
labelText="{{ 'tenant.tenants' | translate }}" labelText="{{ 'tenant.tenants' | translate }}"
placeholderText="{{ 'tenant.tenants' | translate }}" placeholderText="{{ 'tenant.tenants' | translate }}"
@ -80,6 +81,7 @@
<ng-template #tenantProfiles> <ng-template #tenantProfiles>
<tb-entity-list <tb-entity-list
formControlName="tenantProfilesIds" formControlName="tenantProfilesIds"
syncIdsWithDB
subscriptSizing="dynamic" subscriptSizing="dynamic"
labelText="{{ 'tenant-profile.tenant-profiles' | translate }}" labelText="{{ 'tenant-profile.tenant-profiles' | translate }}"
placeholderText="{{ '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, autofocus: false,
branding: false, branding: false,
promotion: 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, relative_urls: false,
urlconverter_callback: (url) => url 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() { 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',
'o3-mini', 'o3-mini',
'o1', 'o1',
'gpt-5.1',
'gpt-5', 'gpt-5',
'gpt-5-mini', 'gpt-5-mini',
'gpt-5-nano', 'gpt-5-nano',
@ -135,6 +136,7 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
AiProvider.GOOGLE_AI_GEMINI, AiProvider.GOOGLE_AI_GEMINI,
{ {
modelList: [ modelList: [
'gemini-3-pro-preview',
'gemini-2.5-pro', 'gemini-2.5-pro',
'gemini-2.5-flash', 'gemini-2.5-flash',
'gemini-2.5-flash-lite', 'gemini-2.5-flash-lite',
@ -149,6 +151,7 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
AiProvider.GOOGLE_VERTEX_AI_GEMINI, AiProvider.GOOGLE_VERTEX_AI_GEMINI,
{ {
modelList: [ modelList: [
'gemini-3-pro-preview',
'gemini-2.5-pro', 'gemini-2.5-pro',
'gemini-2.5-flash', 'gemini-2.5-flash',
'gemini-2.5-flash-lite', 'gemini-2.5-flash-lite',
@ -181,12 +184,10 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
AiProvider.ANTHROPIC, AiProvider.ANTHROPIC,
{ {
modelList: [ modelList: [
'claude-opus-4-5',
'claude-opus-4-1', 'claude-opus-4-1',
'claude-opus-4-0',
'claude-sonnet-4-5', 'claude-sonnet-4-5',
'claude-sonnet-4-0', 'claude-haiku-4-5'
'claude-3-7-sonnet-latest',
'claude-3-5-haiku-latest',
], ],
providerFieldsList: ['apiKey'], providerFieldsList: ['apiKey'],
modelFieldsList: ['temperature', 'topP', 'topK', 'maxOutputTokens'], 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 { export enum EdgeInstructionsMethod {
docker,
ubuntu, ubuntu,
centos, centos
docker
} }
export const edgeVersionAttributeKey = 'edgeVersion'; 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", "data-points-storage-days": "Opbevaringsdage for datapunkter",
"device-api": "Enheds-API", "device-api": "Enheds-API",
"email": "E-mail", "email": "E-mail",
"email-messages": "E-mailbeskeder", "email-messages": "E-mail-beskeder",
"email-messages-daily-activity": "Daglig aktivitet for e-mailbeskeder", "email-messages-daily-activity": "Daglig aktivitet for e-mail-beskeder",
"email-messages-monthly-activity": "Månedlig aktivitet for e-mailbeskeder", "email-messages-monthly-activity": "Månedlig aktivitet for e-mail-beskeder",
"executions": "Udførelser", "executions": "Udførelser",
"scripts": "Scripts", "scripts": "Scripts",
"scripts-hourly-activity": "Timebaseret aktivitet for scripts", "scripts-hourly-activity": "Scriptaktivitet pr. time",
"scripts-daily-activity": "Daglig aktivitet for scripts", "scripts-daily-activity": "Scriptaktivitet pr. dag",
"scripts-monthly-activity": "Månedlig aktivitet for scripts", "scripts-monthly-activity": "Scriptaktivitet pr. måned",
"javascript": "JavaScript", "javascript": "JavaScript",
"javascript-executions": "JavaScript-udførelser", "javascript-executions": "JavaScript-udførelser",
"tbel": "TBEL", "tbel": "TBEL",
@ -1002,15 +1002,15 @@
"type-sms-sent": "SMS sendt" "type-sms-sent": "SMS sendt"
}, },
"debug-settings": { "debug-settings": {
"label": "Fejlsøgningskonfiguration", "label": "Fejlfindingskonfiguration",
"on-failure": "Kun fejl (24/7)", "on-failure": "Kun fejl (24/7)",
"all-messages": "Alle beskeder ({{time}})", "all-messages": "Alle beskeder ({{time}})",
"failures": "Fejl", "failures": "Fejl",
"entity": "enhed", "entity": "enhed",
"hint": { "hint": {
"main-limited": "Alle {{entity}} fejlsøgningsbeskeder vil blive hastighedsbegrænset, med maksimalt {{msg}} beskeder tilladt pr. {{time}}.", "main-limited": "Der vil blive logget højst {{msg}} {{entity}} fejlfindingsbeskeder pr. {{time}}.",
"on-failure": "Gem alle fejlsøgningshændelser uden tidsbegrænsning.", "on-failure": "Log kun fejlbeskeder.",
"all-messages": "Gem alle fejlsøgningshændelser i en tidsbegrænset periode." "all-messages": "Log alle fejlfindingsbeskeder."
} }
}, },
"calculated-fields": { "calculated-fields": {
@ -1111,13 +1111,15 @@
"mistral-ai": "Mistral AI", "mistral-ai": "Mistral AI",
"anthropic": "Anthropic", "anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock", "amazon-bedrock": "Amazon Bedrock",
"github-models": "GitHub-modeller" "github-models": "GitHub-modeller",
"ollama": "Ollama"
}, },
"name-required": "Navn er påkrævet.", "name-required": "Navn er påkrævet.",
"name-max-length": "Navn må højst være 255 tegn.", "name-max-length": "Navn må højst være 255 tegn.",
"provider": "Udbyder", "provider": "Udbyder",
"api-key": "API-nøgle", "api-key": "API-nøgle",
"api-key-required": "API-nøgle er påkrævet.", "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": "Projekt-ID",
"project-id-required": "Projekt-ID er påkrævet", "project-id-required": "Projekt-ID er påkrævet",
"location": "Placering", "location": "Placering",
@ -1152,19 +1154,36 @@
"presence-penalty": "Tilstedeværelsesstraf", "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.", "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": "Frekvensstraf",
"frequency-penalty-hint": "Påfører en straf på et tokens sandsynlighed, som stiger baseret på dets hyppighed i teksten.", "frequency-penalty-hint": "Anvender en straf på sandsynligheden for et token, som stiger afhængigt af dets hyppighed i teksten.",
"max-output-tokens": "Maksimalt outputtokens", "max-output-tokens": "Maksimalt antal output-tokens",
"max-output-tokens-min": "Skal være større end 0.", "max-output-tokens-hint": "Angiver det maksimale antal tokens, som \nmodellen kan generere i ét svar.",
"max-output-tokens-hint": "Angiver det maksimale antal tokens, som modellen 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": "Endpoint",
"endpoint-required": "Endpoint er påkrævet.", "endpoint-required": "Endpoint er påkrævet.",
"baseurl": "Base-URL",
"baseurl-required": "Base-URL er påkrævet.",
"service-version": "Serviceversion", "service-version": "Serviceversion",
"check-connectivity": "Tjek forbindelsen", "check-connectivity": "Tjek forbindelsen",
"check-connectivity-success": "Testanmodningen var vellykket", "check-connectivity-success": "Testanmodningen blev gennemført med succes",
"check-connectivity-failed": "Testanmodningen mislykkedes", "check-connectivity-failed": "Testanmodningen mislykkedes",
"no-model-matching": "Ingen modeller, der matcher '{{entity}}', blev fundet.", "no-model-matching": "Ingen modeller, der matcher '{{entity}}', blev fundet.",
"model-required": "Model er påkrævet.", "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": { "confirm-on-exit": {
"message": "Du har ikke gemte ændringer. Er du sikker på, at du vil forlade denne side?", "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)", "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)" "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", "bootstrap-server": "Bootstrap-server",
"lwm2m-server": "LwM2M-server", "lwm2m-server": "LwM2M-server",
"include-bootstrap-server": "Inkluder Bootstrap Server-opdateringer", "include-bootstrap-server": "Inkludér opdateringer til Bootstrap-server",
"bootstrap-update-title": "Du har allerede konfigureret Bootstrap-server. Er du sikker på, at du vil udelukke opdateringerne?", "bootstrap-update-title": "Du har allerede konfigureret en 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.", "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": "Host",
"server-host-required": "Host er påkrævet.", "server-host-required": "Host er påkrævet.",
"server-port": "Port", "server-port": "Port",
@ -4308,8 +4327,8 @@
"current-password": "Nuværende adgangskode", "current-password": "Nuværende adgangskode",
"copy-jwt-token": "Kopiér JWT-token", "copy-jwt-token": "Kopiér JWT-token",
"jwt-token": "JWT-token", "jwt-token": "JWT-token",
"token-valid-till": "Token er gyldig til", "token-valid-till": "Token er gyldig indtil",
"tokenCopiedSuccessMessage": "JWT-token er kopieret til udklipsholderen", "tokenCopiedSuccessMessage": "JWT-token er blevet kopieret til udklipsholderen",
"tokenCopiedWarnMessage": "JWT-token er udløbet! Opdater venligst siden." "tokenCopiedWarnMessage": "JWT-token er udløbet! Opdater venligst siden."
}, },
"profiles": { "profiles": {
@ -4476,17 +4495,23 @@
"title-max-length": "Titel skal være mindre end 256 tegn", "title-max-length": "Titel skal være mindre end 256 tegn",
"type": { "type": {
"jks": "JKS", "jks": "JKS",
"js-module": "JS modul", "js-module": "JS-modul",
"lwm2m-model": "LWM2M model", "lwm2m-model": "LWM2M-model",
"pkcs-12": "PKCS #12" "pkcs-12": "PKCS #12",
"general": "Generelt"
}, },
"resource-sub-type": "Undertype", "resource-sub-type": "Undertype",
"sub-type": { "sub-type": {
"image": "billede", "image": "billede",
"scada-symbol": "Scada symbol", "scada-symbol": "SCADA-symbol",
"extension": "Udvidelse", "extension": "Udvidelse",
"module": "Modul" "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": { "javascript": {
"add": "Tilføj JavaScript-ressource", "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-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-required": "Cache udløbstid for kunder er påkrævet.",
"customer-cache-expiration-range": "Udløbstiden skal være ≥ 0.", "customer-cache-expiration-range": "Udløbstiden skal være ≥ 0.",
"interval-start": "Interval start", "interval-start": "Start af interval",
"interval-end": "Interval slut", "interval-end": "Intervalslut",
"time-unit": "Tidsenhed", "time-unit": "Tidsenhed",
"fetch-mode": "Hentetilstand", "fetch-mode": "Hentemåde",
"order-by-timestamp": "Sorter efter tidsstempel", "order-by-timestamp": "Sorter efter tidsstempel",
"limit": "Grænse", "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-required": "Grænse er påkrævet.",
"limit-range": "Grænsen skal være mellem 2 og 1000.", "limit-range": "Grænsen skal være mellem 2 og 1000.",
"time-unit-milliseconds": "Millisekunder", "time-unit-milliseconds": "Millisekunder",
@ -4714,9 +4739,9 @@
"time-unit-minutes": "Minutter", "time-unit-minutes": "Minutter",
"time-unit-hours": "Timer", "time-unit-hours": "Timer",
"time-unit-days": "Dage", "time-unit-days": "Dage",
"time-value-range": "Værdien skal være mellem 1 og 2147483647.", "time-value-range": "Tilladt område er fra 1 til 2147483647.",
"start-interval-value-required": "Interval start er påkrævet.", "start-interval-value-required": "Intervalstart er påkrævet.",
"end-interval-value-required": "Interval slut er påkrævet.", "end-interval-value-required": "Intervalslut er påkrævet.",
"filter": "Filter", "filter": "Filter",
"switch": "Skift", "switch": "Skift",
"math-templatization-tooltip": "Understøtter templatisering via $[messageKey] og ${metadataKey}.", "math-templatization-tooltip": "Understøtter templatisering via $[messageKey] og ${metadataKey}.",
@ -4759,7 +4784,7 @@
"copy-from": "Kopiér fra", "copy-from": "Kopiér fra",
"data-to-metadata": "Data til metadata", "data-to-metadata": "Data til metadata",
"metadata-to-data": "Metadata til data", "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": "Interval",
"interval-required": "Interval er påkrævet", "interval-required": "Interval er påkrævet",
"interval-hint": "Dedupliceringsinterval i sekunder.", "interval-hint": "Dedupliceringsinterval i sekunder.",
@ -4812,7 +4837,7 @@
"latest-telemetry": "Seneste telemetri", "latest-telemetry": "Seneste telemetri",
"add-telemetry-key": "Tilføj telemetrinøgle", "add-telemetry-key": "Tilføj telemetrinøgle",
"delete-from": "Slet fra", "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", "fetch-into": "Hent til",
"attr-mapping": "Attributmapping:", "attr-mapping": "Attributmapping:",
"source-attribute": "Kildeattribut", "source-attribute": "Kildeattribut",
@ -4823,11 +4848,11 @@
"target-key-required": "Målnøgle er påkrævet.", "target-key-required": "Målnøgle er påkrævet.",
"attr-mapping-required": "Mindst én mapping skal angives.", "attr-mapping-required": "Mindst én mapping skal angives.",
"fields-mapping": "Feltermapping", "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", "relations-query-config-direction-suffix": "afsender",
"profile-name": "Profilnavn", "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-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": "Metadatafelt '{{perimeterKeyName}}' skal være i formatet: [[48.19736,24.65235],...]", "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.", "short-templatization-tooltip": "Brug $[messageKey] og ${metadataKey} til at hente værdier.",
"fields-mapping-required": "Mindst én feltmapping er påkrævet.", "fields-mapping-required": "Mindst én feltmapping er påkrævet.",
"at-least-one-field-required": "Mindst ét inputfelt skal have en værdi.", "at-least-one-field-required": "Mindst ét inputfelt skal have en værdi.",
@ -4918,8 +4943,8 @@
"mail-body-type": "Indholdstype", "mail-body-type": "Indholdstype",
"body-type-template": "Indholdstypemønster", "body-type-template": "Indholdstypemønster",
"reply-routing-configuration": "Svarrute-konfiguration", "reply-routing-configuration": "Svarrute-konfiguration",
"rpc-reply-routing-configuration-hint": "Konfiguration for at identificere tjeneste, session 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": "Konfiguration for at identificere tjeneste og anmodning i metadata.", "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", "request-id-metadata-attribute": "Anmodnings-id",
"service-id-metadata-attribute": "Tjeneste-id", "service-id-metadata-attribute": "Tjeneste-id",
"session-id-metadata-attribute": "Session-id", "session-id-metadata-attribute": "Session-id",
@ -4947,7 +4972,7 @@
"value-required": "Værdi er påkrævet", "value-required": "Værdi er påkrævet",
"topic-pattern": "Emnemønster", "topic-pattern": "Emnemønster",
"key-pattern": "Nøglemø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-pattern-required": "Emnemønster er påkrævet",
"topic": "Emne", "topic": "Emne",
"topic-required": "Emne er påkrævet", "topic-required": "Emne er påkrævet",
@ -4956,18 +4981,18 @@
"other-properties": "Andre egenskaber", "other-properties": "Andre egenskaber",
"key": "Nøgle", "key": "Nøgle",
"key-required": "Nøgle er påkrævet", "key-required": "Nøgle er påkrævet",
"retries": "Antal genforsøg ved fejl", "retries": "Antal automatiske forsøg ved fejl",
"min-retries-message": "Kun 0 genforsøg tilladt som minimum", "min-retries-message": "Kun 0 som minimumsantal forsøg er tilladt.",
"batch-size-bytes": "Batchstørrelse i bytes", "batch-size-bytes": "Batchstørrelse ved afsendelse (i bytes)",
"min-batch-size-bytes-message": "Minimum batchstørrelse er 0", "min-batch-size-bytes-message": "Kun 0 som minimum batchstørrelse er tilladt.",
"linger-ms": "Buffer-tid lokalt (ms)", "linger-ms": "Tidsbuffer lokalt (ms)",
"min-linger-ms-message": "Minimum 0 ms tilladt", "min-linger-ms-message": "Kun 0 ms som minimumværdi er tilladt.",
"buffer-memory-bytes": "Maks. bufferstørrelse (bytes)", "buffer-memory-bytes": "Maksimal klientbufferstørrelse (i bytes)",
"min-buffer-memory-message": "Minimum bufferstørrelse er 0", "min-buffer-memory-message": "Kun 0 som minimum bufferstørrelse er tilladt.",
"memory-buffer-size-range": "Bufferstørrelse skal være mellem 0 og {{max}} KB", "memory-buffer-size-range": "Bufferstørrelsen skal være mellem 0 og {{max}} KB",
"acks": "Antal kvitteringer", "acks": "Antal bekræftelser",
"topic-arn-pattern": "ARN-emnemønster", "topic-arn-pattern": "Emne-ARN-mønster",
"topic-arn-pattern-required": "ARN-emnemønster er påkrævet", "topic-arn-pattern-required": "Emne-ARN-mønster er påkrævet",
"aws-access-key-id": "AWS Access Key ID", "aws-access-key-id": "AWS Access Key ID",
"aws-access-key-id-required": "AWS Access Key ID er påkrævet", "aws-access-key-id-required": "AWS Access Key ID er påkrævet",
"aws-secret-access-key": "AWS Secret Access Key", "aws-secret-access-key": "AWS Secret Access Key",
@ -4987,35 +5012,35 @@
"password": "Adgangskode", "password": "Adgangskode",
"automatic-recovery": "Automatisk gendannelse", "automatic-recovery": "Automatisk gendannelse",
"connection-timeout-ms": "Forbindelsestimeout (ms)", "connection-timeout-ms": "Forbindelsestimeout (ms)",
"min-connection-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": "Handshake-timeout (ms)", "handshake-timeout-ms": "Tidsgrænse for håndtryk (ms)",
"min-handshake-timeout-ms-message": "Minimum værdi er 0 ms", "min-handshake-timeout-ms-message": "Kun 0 ms som minimumværdi er tilladt.",
"client-properties": "Klientegenskaber", "client-properties": "Klientegenskaber",
"queue-url-pattern": "Kø-URL mønster", "queue-url-pattern": "Køens URL-mønster",
"queue-url-pattern-required": "Kø-URL mønster er påkrævet", "queue-url-pattern-required": "Køens URL-mønster er påkrævet",
"delay-seconds": "Forsinkelse (sekunder)", "delay-seconds": "Forsinkelse (sekunder)",
"min-delay-seconds-message": "Minimum forsinkelse er 0 sekunder", "min-delay-seconds-message": "Kun 0 sekunder som minimumværdi er tilladt.",
"max-delay-seconds-message": "Maksimal forsinkelse er 900 sekunder", "max-delay-seconds-message": "Maksimum tilladt værdi er 900 sekunder.",
"name": "Navn", "name": "Navn",
"name-required": "Navn er påkrævet", "name-required": "Navn er påkrævet.",
"queue-type": "Køtype", "queue-type": "Køtype",
"sqs-queue-standard": "Standard", "sqs-queue-standard": "Standard",
"sqs-queue-fifo": "FIFO", "sqs-queue-fifo": "FIFO",
"gcp-project-id": "GCP-projekt-ID", "gcp-project-id": "GCP-projekt-ID",
"gcp-project-id-required": "GCP-projekt-ID er påkrævet", "gcp-project-id-required": "GCP-projekt-ID er påkrævet",
"gcp-service-account-key": "GCP servicekonto-nøglefil", "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-required": "GCP-servicekonto-nøglefil er påkrævet",
"pubsub-topic-name": "Emnenavn", "pubsub-topic-name": "Emnenavn",
"pubsub-topic-name-required": "Emnenavn er påkrævet", "pubsub-topic-name-required": "Emnenavn er påkrævet",
"message-attributes": "Beskedattributter", "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", "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": "Forbindelsestimeout (sek)", "connect-timeout": "Forbindelsestidsgrænse (sek.)",
"connect-timeout-required": "Forbindelsestimeout er påkrævet.", "connect-timeout-required": "Forbindelsestidsgrænse er påkrævet.",
"connect-timeout-range": "Timeout skal være mellem 1 og 200.", "connect-timeout-range": "Forbindelsestidsgrænsen skal være mellem 1 og 200.",
"client-id": "Klient-ID", "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.", "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 suffix til klient-ID", "append-client-id-suffix": "Tilføj Service-ID som suffiks 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-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": "Enheds-ID",
"device-id-required": "Enheds-ID er påkrævet.", "device-id-required": "Enheds-ID er påkrævet.",
"clean-session": "Ren session", "clean-session": "Ren session",
@ -5043,23 +5068,23 @@
"private-key-password": "Adgangskode til privat nøgle", "private-key-password": "Adgangskode til privat nøgle",
"use-system-smtp-settings": "Brug systemets SMTP-indstillinger", "use-system-smtp-settings": "Brug systemets SMTP-indstillinger",
"use-metadata-dynamic-interval": "Brug dynamisk interval", "use-metadata-dynamic-interval": "Brug dynamisk interval",
"metadata-dynamic-interval-hint": "Intervalstart og -slut felterne understøtter skabeloner. Brug $[messageKey] eller ${metadataKey}.", "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, bruges mønstre fra metadata eller data til interval.", "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", "use-message-alarm-data": "Brug alarmdata fra besked",
"overwrite-alarm-details": "Overskriv alarmdetaljer", "overwrite-alarm-details": "Overskriv alarmdetaljer",
"use-alarm-severity-pattern": "Brug mønster for alarmens alvorlighed", "use-alarm-severity-pattern": "Brug mønster for alarmens alvorlighedsgrad",
"check-all-keys": "Tjek at alle specificerede felter er til stede", "check-all-keys": "Tjek, at alle angivne felter er til stede",
"check-all-keys-hint": "Tjekker at alle nøgler findes i data og metadata.", "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 enhed", "check-relation-to-specific-entity": "Tjek relation til specifik entitet",
"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-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": "Tjek for relation til bestemt eller vilkårlig enhed.", "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 med specifik enhed", "delete-relation-with-specific-entity": "Slet relation til specifik entitet",
"delete-relation-with-specific-entity-hint": "Sletter relation med en bestemt enhed. Ellers slettes alle matchende relationer.", "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 oprindelsen til den angivne enhed eller liste af enheder.", "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": "Fjern nuværende relationer",
"remove-current-relations-hint": "Fjerner nuværende relationer baseret på type og retning.", "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 oprindelse til relateret enhed", "change-originator-to-related-entity": "Skift afsender til relateret entitet",
"change-originator-to-related-entity-hint": "Behandler beskeden som om den kommer fra en anden enhed.", "change-originator-to-related-entity-hint": "Anvendes til at behandle en besked som om den kommer fra en anden entitet.",
"start-interval": "Interval start", "start-interval": "Interval start",
"end-interval": "Interval slut", "end-interval": "Interval slut",
"start-interval-required": "Interval start er påkrævet.", "start-interval-required": "Interval start er påkrævet.",
@ -5119,31 +5144,31 @@
"email-sender": "Email afsender", "email-sender": "Email afsender",
"fields-to-check": "Felter der skal kontrolleres", "fields-to-check": "Felter der skal kontrolleres",
"add-detail": "Tilføj detalje", "add-detail": "Tilføj detalje",
"check-all-keys-tooltip": "Kontrollerer, at alle angivne felter findes i besked og metadata.", "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. Flere felter understøttes.", "fields-to-check-hint": "Tryk på \"Enter\" for at afslutte indtastning af feltnavn. Flere feltnavne understøttes.",
"entity-details-list-empty": "Mindst én detalje skal vælges.", "entity-details-list-empty": "Mindst ét detaljefelt skal vælges.",
"alarm-status": "Alarmstatus", "alarm-status": "Alarmstatus",
"alarm-required": "Mindst én alarmstatus skal vælges.", "alarm-required": "Mindst én alarmstatus skal vælges.",
"no-entity-details-matching": "Ingen tilsvarende entitetsdetaljer fundet.", "no-entity-details-matching": "Ingen matchende entitetsdetaljer blev fundet.",
"custom-table-name": "Brugertabelnavn", "custom-table-name": "Brugertilpasset tabelnavn",
"custom-table-name-required": "Tabelnavn er påkrævet", "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": "Beskedfelt",
"message-field-required": "Beskedfelt er påkrævet.", "message-field-required": "Beskedfelt er påkrævet.",
"table-col": "Tabelkolonne", "table-col": "Tabelkolonne",
"table-col-required": "Tabelkolonne er påkrævet.", "table-col-required": "Tabelkolonne er påkrævet.",
"latitude-field-name": "Navn på breddegradsfelt", "latitude-field-name": "Feltnavn for breddegrad",
"longitude-field-name": "Navn på længdegradsfelt", "longitude-field-name": "Feltnavn for længdegrad",
"latitude-field-name-required": "Navn på breddegradsfelt er påkrævet.", "latitude-field-name-required": "Feltnavn for breddegrad er påkrævet.",
"longitude-field-name-required": "Navn på længdegradsfelt er påkrævet.", "longitude-field-name-required": "Feltnavn for længdegrad er påkrævet.",
"fetch-perimeter-info-from-metadata": "Hent perimeteroplysninger fra metadata", "fetch-perimeter-info-from-metadata": "Hent perimeterinformation fra metadata",
"fetch-perimeter-info-from-metadata-tooltip": "Parser metadatafelt for cirkel eller polygon afhængigt af perimeter-typen.", "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 navn", "perimeter-key-name": "Perimeter-nøgle",
"perimeter-key-name-hint": "Metadata felt med perimeter info.", "perimeter-key-name-hint": "Navn på metadatafelt, der indeholder perimeterinformation.",
"perimeter-key-name-required": "Perimeter nøgle navn er påkrævet.", "perimeter-key-name-required": "Perimeter-nøgle er påkrævet.",
"perimeter-circle": "Cirkel", "perimeter-circle": "Cirkel",
"perimeter-polygon": "Polygon", "perimeter-polygon": "Polygon",
"perimeter-type": "Perimeter type", "perimeter-type": "Perimeter-type",
"circle-center-latitude": "Cirkelens center breddegrad", "circle-center-latitude": "Cirkelens center breddegrad",
"circle-center-latitude-required": "Center breddegrad er påkrævet.", "circle-center-latitude-required": "Center breddegrad er påkrævet.",
"circle-center-longitude": "Cirkelens center længdegrad", "circle-center-longitude": "Cirkelens center længdegrad",
@ -5155,25 +5180,25 @@
"range-unit-nautical-mile": "Sømil", "range-unit-nautical-mile": "Sømil",
"range-units": "Afstandsenheder", "range-units": "Afstandsenheder",
"range-units-required": "Afstandsenheder er påkrævet.", "range-units-required": "Afstandsenheder er påkrævet.",
"range": "Rækkevidde", "range": "Interval",
"range-required": "Rækkevidde er påkrævet.", "range-required": "Interval er påkrævet.",
"polygon-definition": "Polygondefinition", "polygon-definition": "Polygondefinition",
"polygon-definition-required": "Polygondefinition er påkrævet.", "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": "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-inside-duration-time-unit": "Tidsenhed for minimal varighed indenfor",
"min-outside-duration": "Minimal varighed udenfor", "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", "min-outside-duration-time-unit": "Tidsenhed for minimal varighed udenfor",
"tell-failure-if-absent": "Rapporter fejl ved fravær", "tell-failure-if-absent": "Rapportér fejl",
"tell-failure-if-absent-hint": "Hvis mindst én af de valgte nøgler ikke findes, vil udgående besked rapportere \"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 de seneste telemetriværdier", "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;\"", "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": "Ignorér tomme strenge",
"ignore-null-strings-hint": "Ignorerer felter med tomme værdier.", "ignore-null-strings-hint": "Hvis valgt, vil regelnoden ignorere entitetsfelter med tom værdi.",
"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": "Tilføj metadata nøgle-værdi-par til Kafka header",
"add-metadata-key-values-as-kafka-headers-hint": "Hvis valgt, tilføjes metadataens nøgle-værdi par som byte arrays med foruddefineret tegnkodning.", "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": "Tegnkodning",
"charset-encoding-required": "Tegnkodning er påkrævet.", "charset-encoding-required": "Tegnkodning er påkrævet.",
"charset-us-ascii": "US-ASCII", "charset-us-ascii": "US-ASCII",
@ -5182,63 +5207,63 @@
"charset-utf-16be": "UTF-16BE", "charset-utf-16be": "UTF-16BE",
"charset-utf-16le": "UTF-16LE", "charset-utf-16le": "UTF-16LE",
"charset-utf-16": "UTF-16", "charset-utf-16": "UTF-16",
"select-queue-hint": "Vælg et kønavn fra rullemenu eller angiv et brugerdefineret navn.", "select-queue-hint": "Kønavnet kan vælges fra en rullemenu eller angives manuelt.",
"device-profile-node-hint": "Nyttigt ved varighed eller gentagne betingelser for at sikre fortsat evaluering af alarmtilstand.", "device-profile-node-hint": "Nyttigt, hvis du har varigheds- eller gentagelsesbetingelser for at sikre kontinuitet i evalueringen af alarmtilstand.",
"persist-alarm-rules": "Gem tilstand for alarmregler", "persist-alarm-rules": "Bevar tilstand for alarmregler",
"persist-alarm-rules-hint": "Gemmer behandlingsstatus i databasen.", "persist-alarm-rules-hint": "Hvis aktiveret, vil regelnoden gemme behandlingstilstanden i databasen.",
"fetch-alarm-rules": "Hent tilstand for alarmregler", "fetch-alarm-rules": "Hent tilstand for alarmregler",
"fetch-alarm-rules-hint": "Genskaber tilstand ved opstart for at sikre korrekt alarmstatus.", "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": "Input nøgle", "input-value-key": "Inputværdinøgle",
"input-value-key-required": "Input nøgle er påkrævet.", "input-value-key-required": "Inputværdinøgle er påkrævet.",
"output-value-key": "Output nøgle", "output-value-key": "Outputværdinøgle",
"output-value-key-required": "Output nøgle er påkrævet.", "output-value-key-required": "Outputværdinøgle er påkrævet.",
"number-of-digits-after-floating-point": "Antal decimaler", "number-of-digits-after-floating-point": "Antal decimaler efter komma",
"number-of-digits-after-floating-point-range": "Antal decimaler skal være mellem 0 og 15.", "number-of-digits-after-floating-point-range": "Antallet af decimaler skal være mellem 0 og 15.",
"failure-if-delta-negative": "Rapportér fejl ved negativ delta", "failure-if-delta-negative": "Rapportér fejl, hvis delta er negativ",
"failure-if-delta-negative-tooltip": "Fejl ved negativ forskel mellem værdier.", "failure-if-delta-negative-tooltip": "Regelnoden tvinger beskedbehandling til at fejle, hvis delta-værdien er negativ.",
"use-caching": "Brug caching", "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": "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.", "add-time-difference-between-readings-tooltip": "Hvis aktiveret, tilføjer regelnoden \"{{periodValueKey}}\" til den udgående besked.",
"period-value-key": "Tidsintervalnøgle", "period-value-key": "Periodeværdinøgle",
"period-value-key-required": "Tidsintervalnøgle er påkrævet.", "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 meddelelsesindhold.", "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 meddelelsesindhold. Alarmens alvorlighed skal være et systemniveau (CRITICAL, MAJOR osv.)", "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>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.", "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": "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.", "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 skabelonisering. Brug $[messageKey] for værdi fra beskeden og ${metadataKey} for værdi fra metadata.", "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": "Inputfelt understøtter skabelonisering. 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", "shared-scope": "Delt område",
"server-scope": "Serverområde", "server-scope": "Serverområde",
"client-scope": "Klientområde", "client-scope": "Klientområde",
"attribute-type": "Attribute", "attribute-type": "Attribut",
"attribute-type-description": "Hent attributværdi fra database", "attribute-type-description": "Hent attributværdi fra databasen",
"attribute-type-result-description": "Gem som attribut i database", "attribute-type-result-description": "Gem resultatet som en entitetsattribut i databasen",
"constant-type": "Konstant", "constant-type": "Konstant",
"constant-type-description": "Definér konstant værdi", "constant-type-description": "Definér konstant værdi",
"time-series-type": "Tidsserie", "time-series-type": "Tidsserie",
"time-series-type-description": "Hent seneste værdi fra database", "time-series-type-description": "Hent seneste tidsserieværdi fra databasen",
"time-series-type-result-description": "Gem som tidsserie i database", "time-series-type-result-description": "Gem resultatet som en entitets tidsserie i databasen",
"message-body-type": "Besked", "message-body-type": "Besked",
"message-body-type-description": "Hent argumentværdi fra besked", "message-body-type-description": "Hent argumentværdi fra den indgående besked",
"message-body-type-result-description": "Tilføj resultat til besked", "message-body-type-result-description": "Tilføj resultat til den udgående besked",
"message-metadata-type": "Metadata", "message-metadata-type": "Metadata",
"message-metadata-type-description": "Hent argumentværdi fra metadata", "message-metadata-type-description": "Hent argumentværdi fra den indgående beskeds metadata",
"message-metadata-result-description": "Tilføj resultat til metadata", "message-metadata-result-description": "Tilføj resultat til metadata for den udgående besked",
"argument-tile": "Argumenter", "argument-tile": "Argumenter",
"no-arguments-prompt": "Ingen argumenter konfigureret", "no-arguments-prompt": "Ingen argumenter konfigureret",
"result-title": "Resultat", "result-title": "Resultat",
"functions-field-input": "Funktioner", "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": "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": "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": "Konstant værdi",
"constant-value-field-input-required": "Konstant værdi er påkrævet.", "constant-value-field-input-required": "Konstant værdi er påkrævet.",
"attribute-scope-field-input": "Attribut område", "attribute-scope-field-input": "Attributområde",
"attribute-scope-field-input-required": "Område er påkrævet.", "attribute-scope-field-input-required": "Attributområde er påkrævet.",
"default-value-field-input": "Standardværdi", "default-value-field-input": "Standardværdi",
"type-field-input": "Type", "type-field-input": "Type",
"type-field-input-required": "Type er påkrævet.", "type-field-input-required": "Type er påkrævet.",
@ -5266,21 +5291,21 @@
"clear-selected-keys": "Ryd valgte nøgler", "clear-selected-keys": "Ryd valgte nøgler",
"geofence-configuration": "Geofence-konfiguration", "geofence-configuration": "Geofence-konfiguration",
"coordinate-field-names": "Koordinatfeltnavne", "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": "Strategi for tilstedeværelsesovervågning",
"presence-monitoring-strategy-on-first-message": "Ved første besked", "presence-monitoring-strategy-on-first-message": "Ved første besked",
"presence-monitoring-strategy-on-each-message": "Ved hver 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-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 status ved hver besked efter statusændring.", "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", "fetch-credentials-to": "Hent legitimationsoplysninger til",
"add-originator-attributes-to": "Tilføj afsenders attributter til", "add-originator-attributes-to": "Tilføj afsenderattributter til",
"originator-attributes": "Afsenders attributter", "originator-attributes": "Afsenderattributter",
"fetch-latest-telemetry-with-timestamp": "Hent seneste telemetry med tidsstempel", "fetch-latest-telemetry-with-timestamp": "Hent seneste telemetri med tidsstempel",
"fetch-latest-telemetry-with-timestamp-tooltip": "Inkluderer tidsstempel i metadata, f.eks.: \"{{latestTsKeyName}}\": \"{\"ts\":1574329385897, \"value\":42}\"", "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 attribut mangler", "tell-failure": "Rapportér fejl, hvis en eller flere attributter mangler",
"tell-failure-tooltip": "Rapporterer fejl hvis mindst én valgt nøgle mangler.", "tell-failure-tooltip": "Hvis mindst én valgt nøgle ikke findes, vil den udgående besked rapportere \"Fejl\".",
"created-time": "Oprettelsestid", "created-time": "Oprettelsestidspunkt",
"chip-help": "Tryk på 'Enter' for at fuldføre {{inputName}}-indtastning. \nTryk på 'Backspace' for at slette {{inputName}}. \nFlere værdier understøttes.", "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", "detail": "detalje",
"field-name": "feltnavn", "field-name": "feltnavn",
"device-profile": "enhedsprofil", "device-profile": "enhedsprofil",
@ -5291,19 +5316,19 @@
"first-name": "Fornavn", "first-name": "Fornavn",
"last-name": "Efternavn", "last-name": "Efternavn",
"label": "Etiket", "label": "Etiket",
"originator-fields-mapping": "Afsender felttilknytning", "originator-fields-mapping": "Kortlægning af afsenderfelter",
"add-mapped-originator-fields-to": "Tilføj mappede afsenderfelter til", "add-mapped-originator-fields-to": "Tilføj kortlagte afsenderfelter til",
"fields": "Felter", "fields": "Felter",
"skip-empty-fields": "Spring tomme felter over", "skip-empty-fields": "Spring tomme felter over",
"skip-empty-fields-tooltip": "Tomme felter tilføjes ikke til uddata.", "skip-empty-fields-tooltip": "Felter med tomme værdier tilføjes ikke til udgående besked eller metadata.",
"fetch-interval": "Hentningsinterval", "fetch-interval": "Henteinterval",
"fetch-strategy": "Hentningsstrategi", "fetch-strategy": "Hentestrategi",
"fetch-timeseries-from-to": "Hent tidsserier fra {{startInterval}} {{startIntervalTimeUnit}} siden til {{endInterval}} {{endIntervalTimeUnit}} siden.", "fetch-timeseries-from-to": "Hent tidsserie fra {{startInterval}} {{startIntervalTimeUnit}} siden til {{endInterval}} {{endIntervalTimeUnit}} siden.",
"fetch-timeseries-from-to-invalid": "Ugyldig hentning af tidsserier (\"Intervalstart\" skal være mindre end \"Intervalslut\").", "fetch-timeseries-from-to-invalid": "Ugyldig tidsseriehentning (\"Interval start\" skal være mindre end \"Interval slut\").",
"use-metadata-dynamic-interval-tooltip": "Hvis valgt, vil rule node bruge dynamisk intervalstart og -slut baseret på mønstre i beskeden og metadata.", "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 hentningstilstand \"Alle\" er valgt, vil rule node hente telemetri fra intervallet med konfigurerbare forespørgselsparametre.", "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 hentningstilstand \"Første\" er valgt, vil rule node hente den telemetri, der ligger tættest på intervallets start.", "first-mode-hint": "Hvis hentemåde \"Første\" er valgt, hentes den telemetri, der ligger tættest på intervalstarten.",
"last-mode-hint": "Hvis hentningstilstand \"Sidste\" er valgt, vil rule node hente den telemetri, der ligger tættest på intervallets slutning.", "last-mode-hint": "Hvis hentemåde \"Sidste\" er valgt, hentes den telemetri, der ligger tættest på intervalslutningen.",
"ascending": "Stigende", "ascending": "Stigende",
"descending": "Faldende", "descending": "Faldende",
"min": "Min", "min": "Min",
@ -5312,39 +5337,39 @@
"sum": "Sum", "sum": "Sum",
"count": "Antal", "count": "Antal",
"none": "Ingen", "none": "Ingen",
"last-level-relation-tooltip": "Søger kun relationer 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": "Søger kun enheder på det maksimale niveau.", "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", "data-to-fetch": "Data der skal hentes",
"mapping-of-customers": "Kundemapping", "mapping-of-customers": "Kortlægning af kunde",
"map-fields-required": "Alle felter skal udfyldes.", "map-fields-required": "Alle kortlægningsfelter er påkrævede.",
"attributes": "Attributter", "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", "add-selected-attributes-to": "Tilføj valgte attributter til",
"device-profiles": "Enhedsprofiler", "device-profiles": "Enhedsprofiler",
"mapping-of-tenant": "Tenant-mapping", "mapping-of-tenant": "Kortlægning af tenant",
"add-attribute-key": "Tilføj attributnøgle", "add-attribute-key": "Tilføj attributnøgle",
"message-template": "Beskedskabelon", "message-template": "Beskedskabelon",
"message-template-required": "Beskedskabelon er påkrævet", "message-template-required": "Beskedskabelon er påkrævet",
"use-system-slack-settings": "Brug systemets Slack-indstillinger", "use-system-slack-settings": "Brug systemets Slack-indstillinger",
"slack-api-token": "Slack API-token", "slack-api-token": "Slack API-token",
"slack-api-token-required": "Slack API-token er påkrævet", "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", "add-key": "Tilføj nøgle",
"recipients": "Modtagere", "recipients": "Modtagere",
"message-subject-and-content": "Besked emne og indhold", "message-subject-and-content": "Beskedens emne og indhold",
"template-rules-hint": "Brug $[messageKey] eller ${metadataKey} til værdier.", "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 kundens oprindelige afsender.", "originator-customer-desc": "Brug kunde tilhørende beskedens afsender som ny afsender.",
"originator-tenant-desc": "Brug nuværende tenant.", "originator-tenant-desc": "Brug nuværende tenant som ny afsender.",
"originator-related-entity-desc": "Brug relateret entitet som 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 (kun hvis besked er alarm).", "originator-alarm-originator-desc": "Brug alarmens afsender som ny afsender. Kun hvis afsenderen er en alarmenhed.",
"originator-entity-by-name-pattern-desc": "Find og brug entitet ud fra navn og type.", "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] eller ${metadataKey} for at trække værdier.", "email-from-template-hint": "Brug $[messageKey] for værdi fra besked og ${metadataKey} for værdi fra metadata.",
"recipients-block-main-hint": "Kommasepareret liste, understøtter templatization.", "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 standard regelkæde", "forward-msg-default-rule-chain": "Videresend besked til afsenders standardregel-kæde",
"forward-msg-default-rule-chain-tooltip": "Bruger standard regelkæde for afsender eller konfigureret 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": "Udelad nul-delta værdier", "exclude-zero-deltas": "Ekskludér nul-deltaer fra udgående besked",
"exclude-zero-deltas-hint": "Tilføj kun nøgle hvis værdi ikke er nul.", "exclude-zero-deltas-hint": "Hvis aktiveret, tilføjes nøglen \"{{outputValueKey}}\" kun, hvis værdien ikke er nul.",
"exclude-zero-deltas-time-difference-hint": "Tilføj kun hvis outputværdi ≠ 0.", "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-from": "Fra afsender til målentitet",
"search-direction-to": "Fra målentitet til afsender", "search-direction-to": "Fra målentitet til afsender",
"del-relation-direction-from": "Fra afsender", "del-relation-direction-from": "Fra afsender",
@ -5354,33 +5379,33 @@
"function-name": "Funktionsnavn", "function-name": "Funktionsnavn",
"function-name-required": "Funktionsnavn er påkrævet.", "function-name-required": "Funktionsnavn er påkrævet.",
"qualifier": "Kvalifikator", "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", "aws-credentials": "AWS-legitimationsoplysninger",
"connection-timeout": "Forbindelsestimeout", "connection-timeout": "Forbindelsestidsgrænse",
"connection-timeout-required": "Forbindelsestimeout er påkrævet.", "connection-timeout-required": "Forbindelsestidsgrænse er påkrævet.",
"connection-timeout-min": "Minimum forbindelsestimeout er 0.", "connection-timeout-min": "Minimum forbindelsestidsgrænse er 0.",
"connection-timeout-hint": "Ventetid ved etablering af forbindelse. 0 betyder uendelig og frarådes.", "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": "Anmodningstimeout", "request-timeout": "Forespørgsels-timeout",
"request-timeout-required": "Anmodningstimeout er påkrævet", "request-timeout-required": "Forespørgsels-timeout er påkrævet.",
"request-timeout-min": "Minimum anmodningstimeout er 0", "request-timeout-min": "Minimum forespørgsels-timeout er 0.",
"request-timeout-hint": "Ventetid for anmodning, før timeout. 0 betyder uendelig og frarådes.", "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", "units": "Enheder",
"tell-failure-aws-lambda": "Rapportér fejl hvis AWS Lambda-funktion kaster en undtagelse", "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.", "tell-failure-aws-lambda-hint": "Regelnoden tvinger fejl i beskedbehandling, hvis AWS Lambda-funktion udløser en undtagelse.",
"basic-mode": "Grundlæggende", "basic-mode": "Basis",
"advanced-mode": "Avanceret", "advanced-mode": "Avanceret",
"save-time-series": { "save-time-series": {
"processing-settings": "Behandlingsindstillinger", "processing-settings": "Behandlingsindstillinger",
"processing-settings-hint": "Definér hvordan beskeder behandles. Grundlæggende eller avancerede strategier.", "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 – visse kombinationer kan føre til uventet adfærd.", "advanced-settings-hint": "Vær forsigtig ved konfiguration af behandlingsstrategier. Visse kombinationer kan føre til uforudset opførsel.",
"strategy": "Strategi", "strategy": "Strategi",
"deduplication-interval": "Deduplikeringsinterval", "deduplication-interval": "Deduplikationsinterval",
"deduplication-interval-required": "Dedupliceringsinterval er påkrævet", "deduplication-interval-required": "Deduplikationsinterval er påkrævet",
"deduplication-interval-min-max-range": "Skal være mellem 1 sekund og 1 dag", "deduplication-interval-min-max-range": "Deduplikationsinterval skal være mindst 1 sekund og højst 1 dag",
"strategy-type": { "strategy-type": {
"every-message": "Ved hver besked", "every-message": "Ved hver besked",
"skip": "Spring over", "skip": "Spring over",
"deduplicate": "Dedupliker", "deduplicate": "Deduplikér",
"web-sockets-only": "Kun WebSockets" "web-sockets-only": "Kun WebSockets"
}, },
"time-series": "Tidsserier", "time-series": "Tidsserier",
@ -5390,17 +5415,17 @@
}, },
"save-attribute": { "save-attribute": {
"processing-settings": "Behandlingsindstillinger", "processing-settings": "Behandlingsindstillinger",
"processing-settings-hint": "Definér hvordan beskeder behandles. Grundlæggende eller avancerede strategier.", "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 – visse kombinationer kan føre til uventet adfærd.", "advanced-settings-hint": "Vær forsigtig ved konfiguration af behandlingsstrategier. Visse kombinationer kan føre til uforudset opførsel.",
"strategy": "Strategi", "strategy": "Strategi",
"deduplication-interval": "Deduplikeringsinterval", "deduplication-interval": "Deduplikationsinterval",
"deduplication-interval-required": "Deduplikeringsinterval er påkrævet", "deduplication-interval-required": "Deduplikationsinterval er påkrævet",
"deduplication-interval-min-max-range": "Skal være mellem 1 sekund og 1 dag", "deduplication-interval-min-max-range": "Deduplikationsinterval skal være mindst 1 sekund og højst 1 dag",
"scope": "Omfang", "scope": "Omfang",
"strategy-type": { "strategy-type": {
"every-message": "Ved hver besked", "every-message": "Ved hver besked",
"skip": "Spring over", "skip": "Spring over",
"deduplicate": "Dedupliker", "deduplicate": "Deduplikér",
"web-sockets-only": "Kun WebSockets" "web-sockets-only": "Kun WebSockets"
}, },
"attributes": "Attributter" "attributes": "Attributter"
@ -5432,32 +5457,33 @@
"ai": { "ai": {
"ai-model": "AI-model", "ai-model": "AI-model",
"model": "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.", "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": "Promptindstillinger", "prompt-settings": "Prompt-indstillinger",
"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.", "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": "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.", "system-prompt-blank": "Systemprompt må ikke være tom.",
"user-prompt": "Brugerprompt", "user-prompt": "Brugerprompt",
"user-prompt-required": "Brugerprompt er påkrævet.", "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.", "user-prompt-blank": "Brugerprompt må ikke være tom.",
"response-format": "Svarformat", "response-format": "Svartype",
"response-text": "Tekst", "response-text": "Tekst",
"response-json": "JSON", "response-json": "JSON",
"response-json-schema": "JSON-skema", "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-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 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 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 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-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 rule node kun et begrænset sæt funktioner. Se dokumentationen for detaljer.", "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", "response-json-schema-required": "JSON-skema er påkrævet",
"advanced-settings": "Avancerede indstillinger", "advanced-settings": "Avancerede indstillinger",
"timeout": "Timeout", "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-required": "Timeout er påkrævet",
"timeout-validation": "Skal være mellem 1 sekund og 10 minutter.", "timeout-validation": "Skal være mellem 1 sekund og 10 minutter.",
"force-acknowledgement": "Tving kvittering", "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": { "timezone": {
@ -5757,9 +5783,9 @@
"rule-engine-exceptions-ttl-days": "Rule Engine-undtagelser TTL dage", "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-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", "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": "Maks. antal regelnodekørsler 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-required": "Maks. antal regelnodekørsler 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-range": "Maks. antal regelnodekørsler pr. besked må ikke være negativ",
"max-emails": "Maksimalt antal sendte e-mails", "max-emails": "Maksimalt antal sendte e-mails",
"max-emails-required": "Maksimalt antal sendte e-mails er påkrævet.", "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", "max-emails-range": "Maksimalt antal sendte e-mails må ikke være negativt",
@ -5987,9 +6013,10 @@
"label": "Etiket", "label": "Etiket",
"value": "Værdi", "value": "Værdi",
"date": "Dato", "date": "Dato",
"show-date-time-interval": "Vis datotidsinterval", "show-date-time-interval": "Vis dato-/tidsinterval",
"show-date-time-interval-hint": "Vis datotidsinterval i henhold til dataaggregering.", "show-date-time-interval-hint": "Vis dato-/tidsinterval i henhold til dataaggregering.",
"hide-zero-tooltip-values": "Skjul nulværdier", "hide-zero-tooltip-values": "Skjul nulværdier",
"show-stack-total": "Vis totalværdi i stablet tilstand",
"background-color": "Baggrundsfarve", "background-color": "Baggrundsfarve",
"background-blur": "Baggrundssløring" "background-blur": "Baggrundssløring"
}, },
@ -6702,15 +6729,15 @@
"no-widgets-text": "Ingen widgets fundet", "no-widgets-text": "Ingen widgets fundet",
"management": "Widgetadministration", "management": "Widgetadministration",
"editor": "Widget-editor", "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?", "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 at indlæse widgetkonfiguration.<br>Den tilknyttede widgettype er sandsynligvis blevet fjernet.", "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:", "widget-type-load-error": "Widget blev ikke indlæst på grund af følgende fejl:",
"remove": "Fjern widget", "remove": "Fjern widget",
"delete": "Slet widget", "delete": "Slet widget",
"edit": "Rediger widget", "edit": "Rediger widget",
"remove-widget-title": "Er du sikker på, at du vil fjerne widgetten '{{widgetTitle}}'?", "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.", "remove-widget-text": "Efter bekræftelse bliver widgetten og alle relaterede data uigenkaldeligt slettet.",
"replace-reference-with-widget-copy": "Erstat reference med kopi af widget", "replace-reference-with-widget-copy": "Erstat reference med en kopi af widget",
"timeseries": "Tidsserier", "timeseries": "Tidsserier",
"search-data": "Søg data", "search-data": "Søg data",
"no-data-found": "Ingen data fundet", "no-data-found": "Ingen data fundet",
@ -7550,8 +7577,8 @@
"October": "Oktober", "October": "Oktober",
"November": "November", "November": "November",
"December": "December", "December": "December",
"Custom Date Range": "Brugerdefineret datoområde", "Custom Date Range": "Brugerdefineret datointerval",
"Date Range Template": "Skabelon for datoområde", "Date Range Template": "Datointervalskabelon",
"Today": "I dag", "Today": "I dag",
"Yesterday": "I går", "Yesterday": "I går",
"This Week": "Denne uge", "This Week": "Denne uge",
@ -7572,7 +7599,7 @@
"Custom interval": "Brugerdefineret interval", "Custom interval": "Brugerdefineret interval",
"Interval": "Interval", "Interval": "Interval",
"Step size": "Trinstørrelse", "Step size": "Trinstørrelse",
"Ok": "OK" "Ok": "Ok"
} }
}, },
"doughnut": { "doughnut": {
@ -7784,11 +7811,11 @@
"gpio-switches": "GPIO-kontakter", "gpio-switches": "GPIO-kontakter",
"no-gpio-switches": "Ingen GPIO-kontakter konfigureret", "no-gpio-switches": "Ingen GPIO-kontakter konfigureret",
"add-gpio-switch": "Tilføj GPIO-kontakt", "add-gpio-switch": "Tilføj GPIO-kontakt",
"gpio-status-request": "GPIO-statusanmodning", "gpio-status-request": "GPIO-statusforespørgsel",
"method-name": "Metodenavn", "method-name": "Metodenavn",
"method-body": "Metodeindhold", "method-body": "Metodeindhold",
"gpio-status-change-request": "Anmodning om ændring af GPIO-status", "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", "gpio-leds": "GPIO-LED'er",
"no-gpio-leds": "Ingen GPIO-LED'er konfigureret", "no-gpio-leds": "Ingen GPIO-LED'er konfigureret",
"add-gpio-led": "Tilføj GPIO-LED" "add-gpio-led": "Tilføj GPIO-LED"
@ -9522,4 +9549,4 @@
"language": { "language": {
"language": "Sprog" "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-secret-access-key-required": "AWS Secret Access Key ist erforderlich",
"aws-region": "AWS-Region", "aws-region": "AWS-Region",
"aws-region-required": "AWS-Region ist erforderlich", "aws-region-required": "AWS-Region ist erforderlich",
"number-from": "Absendernummer", "number-from": "Telefonnummer Von",
"number-from-required": "Absendernummer ist erforderlich.", "number-from-required": "Telefonnummer Von ist erforderlich.",
"number-to": "Empfängernummer", "number-to": "Telefonnummer An",
"number-to-required": "Empfängernummer ist erforderlich.", "number-to-required": "Telefonnummer An ist erforderlich.",
"phone-number-hint": "Telefonnummer im E.164-Format, z. B. +19995550123", "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-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 sein, z. B. +19995550123.", "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/Messaging Service SID sein.", "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": "SMS-Nachricht",
"sms-message-required": "SMS-Nachricht ist erforderlich.", "sms-message-required": "SMS-Nachricht ist erforderlich.",
"sms-message-max-length": "SMS-Nachricht darf nicht länger als 1600 Zeichen sein", "sms-message-max-length": "SMS-Nachricht darf nicht länger als 1600 Zeichen sein",
"twilio-account-sid": "Twilio Account SID", "twilio-account-sid": "Twilio-Konto SID",
"twilio-account-sid-required": "Twilio Account SID ist erforderlich", "twilio-account-sid-required": "Twilio-Konto SID ist erforderlich",
"twilio-account-token": "Twilio Account Token", "twilio-account-token": "Twilio-Konto-Token",
"twilio-account-token-required": "Twilio Account Token ist erforderlich", "twilio-account-token-required": "Twilio-Konto-Token ist erforderlich",
"send-test-sms": "Test-SMS senden", "send-test-sms": "Test-SMS senden",
"test-sms-sent": "Test-SMS wurde erfolgreich gesendet!", "test-sms-sent": "Test-SMS wurde erfolgreich gesendet!",
"security-settings": "Sicherheitseinstellungen", "security-settings": "Sicherheitseinstellungen",
"password-policy": "Passwortrichtlinie", "password-policy": "Passwortrichtlinie",
"minimum-password-length": "Minimale Passwortlänge", "minimum-password-length": "Minimale Passwortlänge",
"minimum-password-length-required": "Minimale Passwortlänge ist erforderlich", "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": "Maximale Passwortlänge",
"maximum-password-length-min": "Maximale Passwortlänge sollte mindestens 6 betragen", "maximum-password-length-min": "Die maximale Passwortlänge muss mindestens 6 betragen",
"maximum-password-length-less-min": "Maximale Passwortlänge muss größer als die minimale Länge sein", "maximum-password-length-less-min": "Die maximale Passwortlänge muss größer als die minimale sein",
"minimum-uppercase-letters": "Minimale Anzahl an Großbuchstaben", "minimum-uppercase-letters": "Mindestanzahl an Großbuchstaben",
"minimum-uppercase-letters-range": "Minimale Anzahl an Großbuchstaben darf nicht negativ sein", "minimum-uppercase-letters-range": "Die Mindestanzahl an Großbuchstaben darf nicht negativ sein",
"minimum-lowercase-letters": "Minimale Anzahl an Kleinbuchstaben", "minimum-lowercase-letters": "Mindestanzahl an Kleinbuchstaben",
"minimum-lowercase-letters-range": "Minimale Anzahl an Kleinbuchstaben darf nicht negativ sein", "minimum-lowercase-letters-range": "Die Mindestanzahl an Kleinbuchstaben darf nicht negativ sein",
"minimum-digits": "Minimale Anzahl an Ziffern", "minimum-digits": "Mindestanzahl an Ziffern",
"minimum-digits-range": "Minimale Anzahl an Ziffern darf nicht negativ sein", "minimum-digits-range": "Die Mindestanzahl an Ziffern darf nicht negativ sein",
"minimum-special-characters": "Minimale Anzahl an Sonderzeichen", "minimum-special-characters": "Mindestanzahl an Sonderzeichen",
"minimum-special-characters-range": "Minimale Anzahl an Sonderzeichen darf nicht negativ sein", "minimum-special-characters-range": "Die Mindestanzahl an Sonderzeichen darf nicht negativ sein",
"password-expiration-period-days": "Passwort-Ablaufzeitraum in Tagen", "password-expiration-period-days": "Passwort-Ablaufzeitraum in Tagen",
"password-expiration-period-days-range": "Passwort-Ablaufzeitraum darf nicht negativ sein", "password-expiration-period-days-range": "Passwort-Ablaufzeitraum darf nicht negativ sein",
"password-reuse-frequency-days": "Häufigkeit der Passwortwiederverwendung in Tagen", "password-reuse-frequency-days": "Häufigkeit der Passwortwiederverwendung in Tagen",
@ -676,8 +676,8 @@
"alias": { "alias": {
"add": "Alias hinzufügen", "add": "Alias hinzufügen",
"edit": "Alias bearbeiten", "edit": "Alias bearbeiten",
"name": "Aliasname", "name": "Alias-Name",
"name-required": "Aliasname ist erforderlich", "name-required": "Alias-Name ist erforderlich",
"duplicate-alias": "Ein Alias mit demselben Namen existiert bereits.", "duplicate-alias": "Ein Alias mit demselben Namen existiert bereits.",
"filter-type-single-entity": "Einzelne Entität", "filter-type-single-entity": "Einzelne Entität",
"filter-type-entity-list": "Entitätsliste", "filter-type-entity-list": "Entitätsliste",
@ -864,15 +864,15 @@
"api-features": "API-Funktionen", "api-features": "API-Funktionen",
"api-usage": "API-Nutzung", "api-usage": "API-Nutzung",
"alarm": "Alarm", "alarm": "Alarm",
"alarms-created": "Erstellte Alarme", "alarms-created": "Alarme erstellt",
"queue-stats": "Warteschlangenstatistiken", "queue-stats": "Warteschlangenstatistiken",
"processing-failures-and-timeouts": "Verarbeitungsfehler und Zeitüberschreitungen", "processing-failures-and-timeouts": "Verarbeitungsfehler und Zeitüberschreitungen",
"exceptions": "Ausnahmen", "exceptions": "Ausnahmen",
"alarms-created-daily-activity": "Tägliche Aktivität der Alarmerstellung", "alarms-created-daily-activity": "Tägliche Aktivität der Alarm-Erstellung",
"alarms-created-hourly-activity": "Stündliche Aktivität der Alarmerstellung", "alarms-created-hourly-activity": "Stündliche Aktivität der Alarm-Erstellung",
"alarms-created-monthly-activity": "Monatliche Aktivität der Alarmerstellung", "alarms-created-monthly-activity": "Monatliche Aktivität der Alarm-Erstellung",
"data-points": "Datenpunkte", "data-points": "Datenpunkte",
"data-points-storage-days": "Speichertage der Datenpunkte", "data-points-storage-days": "Speicherzeitraum der Datenpunkte (in Tagen)",
"device-api": "Geräte-API", "device-api": "Geräte-API",
"email": "E-Mail", "email": "E-Mail",
"email-messages": "E-Mail-Nachrichten", "email-messages": "E-Mail-Nachrichten",
@ -1111,13 +1111,15 @@
"mistral-ai": "Mistral AI", "mistral-ai": "Mistral AI",
"anthropic": "Anthropic", "anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock", "amazon-bedrock": "Amazon Bedrock",
"github-models": "GitHub-Modelle" "github-models": "GitHub-Modelle",
"ollama": "Ollama"
}, },
"name-required": "Name ist erforderlich.", "name-required": "Name ist erforderlich.",
"name-max-length": "Der Name darf höchstens 255 Zeichen lang sein.", "name-max-length": "Der Name darf höchstens 255 Zeichen lang sein.",
"provider": "Anbieter", "provider": "Anbieter",
"api-key": "API-Schlüssel", "api-key": "API-Schlüssel",
"api-key-required": "API-Schlüssel ist erforderlich.", "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": "Projekt-ID",
"project-id-required": "Projekt-ID ist erforderlich.", "project-id-required": "Projekt-ID ist erforderlich.",
"location": "Standort", "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-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-p-min-max": "Muss größer als 0 und maximal 1 sein.",
"top-k": "Top K", "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.", "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.", "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": "Frequenzstrafe",
"frequency-penalty-hint": "Verringert die Wahrscheinlichkeit eines Tokens basierend auf seiner Häufigkeit im Text.", "frequency-penalty-hint": "Reduziert die Wahrscheinlichkeit eines Tokens abhängig von seiner Häufigkeit im Text.",
"max-output-tokens": "Maximale Ausgabetokens", "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 \nModell in einer Antwort generieren kann.",
"max-output-tokens-hint": "Legt die maximale Anzahl an Tokens fest, die das Modell 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": "Endpunkt",
"endpoint-required": "Endpunkt ist erforderlich.", "endpoint-required": "Endpunkt ist erforderlich.",
"baseurl": "Basis-URL",
"baseurl-required": "Basis-URL ist erforderlich.",
"service-version": "Service-Version", "service-version": "Service-Version",
"check-connectivity": "Konnektivität prüfen", "check-connectivity": "Konnektivität prüfen",
"check-connectivity-success": "Testanfrage war erfolgreich", "check-connectivity-success": "Testanfrage war erfolgreich",
"check-connectivity-failed": "Testanfrage fehlgeschlagen", "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.", "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": { "confirm-on-exit": {
"message": "Sie haben ungespeicherte Änderungen. Möchten Sie diese Seite wirklich verlassen?", "message": "Sie haben ungespeicherte Änderungen. Möchten Sie diese Seite wirklich verlassen?",
@ -1228,7 +1247,7 @@
"required-fields": "Erforderliche Felder fehlen" "required-fields": "Erforderliche Felder fehlen"
}, },
"content-type": { "content-type": {
"json": "Json", "json": "JSON",
"text": "Text", "text": "Text",
"binary": "Binär (Base64)" "binary": "Binär (Base64)"
}, },
@ -1261,11 +1280,11 @@
"add-customer-text": "Neuen Kunden hinzufügen", "add-customer-text": "Neuen Kunden hinzufügen",
"no-customers-text": "Keine Kunden gefunden", "no-customers-text": "Keine Kunden gefunden",
"customer-details": "Kundendetails", "customer-details": "Kundendetails",
"delete-customer-title": "Möchten Sie den Kunden '{{customerTitle}}' 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 unwiederbringlich gelöscht.", "delete-customer-text": "Achtung: Nach der Bestätigung werden der Kunde und alle zugehörigen Daten unwiderruflich gelöscht.",
"delete-customers-title": "Möchten Sie { count, plural, =1 {1 Kunden} other {# Kunden} } wirklich löschen?", "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-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-users": "Benutzer verwalten",
"manage-assets": "Assets verwalten", "manage-assets": "Assets verwalten",
"manage-devices": "Geräte verwalten", "manage-devices": "Geräte verwalten",
@ -1578,13 +1597,13 @@
"time-prev-description": "Zeitstempel des vorherigen Werts;", "time-prev-description": "Zeitstempel des vorherigen Werts;",
"prev-orig-value-description": "ursprünglicher vorheriger Wert;", "prev-orig-value-description": "ursprünglicher vorheriger Wert;",
"aggregation": "Aggregation", "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-none-hint": "Letzten Wert übernehmen.",
"aggregation-type-min-hint": "Mindestwert innerhalb des ausgewählten Zeitfensters finden.", "aggregation-type-min-hint": "Minimalwert unter den Datenpunkten innerhalb eines ausgewählten Zeitfensters ermitteln.",
"aggregation-type-max-hint": "Höchstwert innerhalb des ausgewählten Zeitfensters finden.", "aggregation-type-max-hint": "Maximalwert unter den Datenpunkten innerhalb eines ausgewählten Zeitfensters ermitteln.",
"aggregation-type-avg-hint": "Durchschnittswert innerhalb des ausgewählten Zeitfensters berechnen.", "aggregation-type-avg-hint": "Durchschnittswert der Datenpunkte innerhalb eines ausgewählten Zeitfensters berechnen.",
"aggregation-type-sum-hint": "Alle Werte der Datenpunkte im Zeitfenster summieren.", "aggregation-type-sum-hint": "Summe aller Werte der Datenpunkte innerhalb eines ausgewählten Zeitfensters berechnen.",
"aggregation-type-count-hint": "Gesamtanzahl der Datenpunkte im Zeitfenster.", "aggregation-type-count-hint": "Gesamtanzahl der Datenpunkte innerhalb eines ausgewählten Zeitfensters.",
"delta-calculation": "Delta-Berechnung", "delta-calculation": "Delta-Berechnung",
"enable-delta-calculation": "Delta-Berechnung aktivieren", "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.", "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!", "create-new-device-profile": "Neues erstellen!",
"mqtt-device-topic-filters": "MQTT-Gerät-Topic-Filter", "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-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": "MQTT Sparkplug B Edge-of-Network (EoN)-Knoten.",
"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-hint": "Erlaubt Verbindungen von EoN-Knoten mit Sparkplug B Payload- 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": "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 Telemetrie gespeichert.", "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-Nutzlast", "mqtt-device-payload-type": "MQTT-Geräte-Payload",
"mqtt-device-payload-type-json": "JSON", "mqtt-device-payload-type-json": "JSON",
"mqtt-device-payload-type-proto": "Protobuf", "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": "Kompatibilität mit anderen Payload-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-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-Themen verwenden", "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, 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-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 Validierungsfehler senden", "mqtt-send-ack-on-validation-exception": "PUBACK bei PUBLISH-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-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", "mqtt-protocol-version": "Protokollversion",
"snmp-add-mapping": "SNMP-Zuordnung hinzufügen", "snmp-add-mapping": "SNMP-Zuordnung hinzufügen",
"snmp-mapping-not-configured": "Keine Zuordnung für OID zu Zeitreihen/Telemetrie konfiguriert", "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": "RPC-Anfrage-Protoschema",
"rpc-request-proto-schema-required": "RPC-Anfrage-Protoschema ist erforderlich.", "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.", "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-pattern-topic-filter": "Ungültiges Topic-Filter-Muster",
"not-valid-single-character": "Ungültige Verwendung eines Single-Level-Wildcards", "not-valid-single-character": "Ungültige Verwendung eines einstelligen Platzhalters",
"not-valid-multi-character": "Ungültige Verwendung eines Multi-Level-Wildcards", "not-valid-multi-character": "Ungültige Verwendung eines mehrstufigen Platzhalters",
"single-level-wildcards-hint": "<code>[+]</code> kann jedes Topic-Level ersetzen. Beispiel: <b>v1/devices/+/telemetry</b>.", "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> ersetzt mehrere Ebenen und muss am Ende des Topics stehen. Beispiel: <b>#</b> oder <b>v1/devices/me/#</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": "Alarmregeln",
"alarm-rules-with-count": "Alarmregeln ({{count}})", "alarm-rules-with-count": "Alarmregeln ({{count}})",
"no-alarm-rules": "Keine Alarmregeln konfiguriert", "no-alarm-rules": "Keine Alarmregeln konfiguriert",
@ -2050,8 +2069,8 @@
"edit-alarm-rule": "Alarmregel bearbeiten", "edit-alarm-rule": "Alarmregel bearbeiten",
"alarm-type": "Alarmtyp", "alarm-type": "Alarmtyp",
"alarm-type-required": "Alarmtyp ist erforderlich.", "alarm-type-required": "Alarmtyp ist erforderlich.",
"alarm-type-unique": "Alarmtyp muss innerhalb der Alarmregeln des Geräteprofils eindeutig sein.", "alarm-type-unique": "Der Alarmtyp muss innerhalb der Geräteprofil-Alarmregeln eindeutig sein.",
"alarm-type-max-length": "Alarmtyp sollte weniger als 256 Zeichen enthalten", "alarm-type-max-length": "Alarmtyp darf maximal 256 Zeichen lang sein",
"create-alarm-pattern": "<b>{{alarmType}}</b> Alarm erstellen", "create-alarm-pattern": "<b>{{alarmType}}</b> Alarm erstellen",
"create-alarm-rules": "Alarmregeln erstellen", "create-alarm-rules": "Alarmregeln erstellen",
"no-create-alarm-rules": "Keine Erstellungsbedingungen konfiguriert", "no-create-alarm-rules": "Keine Erstellungsbedingungen konfiguriert",
@ -2072,16 +2091,16 @@
"advanced-settings": "Erweiterte Einstellungen", "advanced-settings": "Erweiterte Einstellungen",
"alarm-rule-additional-info": "Zusätzliche Informationen", "alarm-rule-additional-info": "Zusätzliche Informationen",
"edit-alarm-rule-additional-info": "Zusätzliche Informationen bearbeiten", "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-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> zur Ersetzung von Attribut- oder Telemetriedaten.", "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": "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", "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": "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.", "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 (Kunde oder Mandant) propagieren", "propagate-alarm-to-owner": "Alarm an Eigentümer der Entität weiterleiten (Kunde oder Tenant)",
"propagate-alarm-to-tenant": "Alarm an Mandant propagieren", "propagate-alarm-to-tenant": "Alarm an den Tenant weiterleiten",
"alarm-rule-condition": "Alarmregelbedingung", "alarm-rule-condition": "Alarmregelbedingung",
"enter-alarm-rule-condition-prompt": "Bitte fügen Sie eine Alarmregelbedingung hinzu", "enter-alarm-rule-condition-prompt": "Bitte fügen Sie eine Alarmregelbedingung hinzu",
"edit-alarm-rule-condition": "Alarmregelbedingung bearbeiten", "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)", "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)" "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", "bootstrap-server": "Bootstrap-Server",
"lwm2m-server": "LwM2M-Server", "lwm2m-server": "LwM2M-Server",
"include-bootstrap-server": "Bootstrap-Server-Updates einbeziehen", "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-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": "Host",
"server-host-required": "Host ist erforderlich.", "server-host-required": "Host ist erforderlich.",
"server-port": "Port", "server-port": "Port",
"server-port-required": "Port ist erforderlich.", "server-port-required": "Port ist erforderlich.",
"server-port-pattern": "Port muss eine positive Ganzzahl sein.", "server-port-pattern": "Port muss eine positive ganze Zahl sein.",
"server-port-range": "Port sollte im Bereich von 1 bis 65535 liegen.", "server-port-range": "Port muss im Bereich von 1 bis 65535 liegen.",
"server-public-key": "Öffentlicher Schlüssel des Servers", "server-public-key": "Öffentlicher Schlüssel des Servers",
"server-public-key-required": "Öffentlicher Schlüssel des Servers ist erforderlich.", "server-public-key-required": "Öffentlicher Schlüssel des Servers ist erforderlich.",
"client-hold-off-time": "Wartezeit des Clients", "client-hold-off-time": "Verzögerungszeit (Hold Off Time)",
"client-hold-off-time-required": "Wartezeit des Clients ist erforderlich.", "client-hold-off-time-required": "Verzögerungszeit ist erforderlich.",
"client-hold-off-time-pattern": "Wartezeit des Clients muss eine positive Ganzzahl sein.", "client-hold-off-time-pattern": "Verzögerungszeit muss eine positive ganze Zahl sein.",
"client-hold-off-time-tooltip": "Client-Wartezeit nur bei Verwendung eines Bootstrap-Servers", "client-hold-off-time-tooltip": "Verzögerungszeit für den Einsatz mit einem Bootstrap-Server",
"account-after-timeout": "Konto nach Zeitüberschreitung", "account-after-timeout": "Aktivierung nach Zeitüberschreitung",
"account-after-timeout-required": "Konto nach Zeitüberschreitung ist erforderlich.", "account-after-timeout-required": "Aktivierung nach Zeitüberschreitung ist erforderlich.",
"account-after-timeout-pattern": "Konto nach Zeitüberschreitung muss eine positive Ganzzahl sein.", "account-after-timeout-pattern": "Der Wert muss eine positive ganze Zahl sein.",
"account-after-timeout-tooltip": "Wert für Konto nach Zeitüberschreitung beim Bootstrap-Server.", "account-after-timeout-tooltip": "Bootstrap-Server-Aktivierung nach dem durch diese Ressource angegebenen Timeout-Wert.",
"server-type": "Servertyp", "server-type": "Servertyp",
"add-new-server-title": "Neue Serverkonfiguration hinzufügen", "add-new-server-title": "Neue Serverkonfiguration hinzufügen",
"add-server-config": "Serverkonfiguration hinzufügen", "add-server-config": "Serverkonfiguration hinzufügen",
@ -2267,25 +2286,25 @@
"others-tab": "Weitere Einstellungen", "others-tab": "Weitere Einstellungen",
"ota-update": "OTA-Update", "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": "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).", "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 Verbinden", "client-strategy": "Client-Strategie beim Verbindungsaufbau",
"client-strategy-label": "Strategie", "client-strategy-label": "Strategie",
"client-strategy-only-observe": "Nur Beobachtungsanfragen nach der ersten Verbindung", "client-strategy-only-observe": "Nur Observe-Anfrage an den Client nach der ersten Verbindung",
"client-strategy-read-all": "Alle Ressourcen lesen und beobachten nach der Registrierung", "client-strategy-read-all": "Alle Ressourcen lesen und Observe-Anfrage nach der Registrierung senden",
"fw-update": "Firmware-Update", "fw-update": "Firmware-Update",
"fw-update-strategy": "Firmware-Update-Strategie", "fw-update-strategy": "Strategie für Firmware-Update",
"fw-update-strategy-data": "Firmware-Update als Binärdatei mit Objekt 19 und Ressource 0 (Daten) übertragen", "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 mit Objekt 5 und Ressource 0 (Paket) ü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, um das Paket herunterzuladen, und als Objekt 5 und Ressource 1 (Paket-URI) ü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": "Software-Update",
"sw-update-strategy": "Software-Update-Strategie", "sw-update-strategy": "Strategie für Software-Update",
"sw-update-strategy-package": "Binärdatei mit Objekt 9 und Ressource 2 (Paket) übertragen", "sw-update-strategy-package": "Binärdatei über Objekt 9 und Ressource 2 (Package) ü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", "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": "Firmware-Update CoAP-Ressource", "fw-update-resource": "CoAP-Ressource für Firmware-Update",
"fw-update-resource-required": "Firmware-Update CoAP-Ressource ist erforderlich.", "fw-update-resource-required": "CoAP-Ressource für Firmware-Update ist erforderlich.",
"sw-update-resource": "Software-Update CoAP-Ressource", "sw-update-resource": "CoAP-Ressource für Software-Update",
"sw-update-resource-required": "Software-Update CoAP-Ressource ist erforderlich.", "sw-update-resource-required": "CoAP-Ressource für Software-Update ist erforderlich.",
"config-json-tab": "JSON-Konfigurationsprofil Gerät", "config-json-tab": "JSON-Konfigurationsprofil für Gerät",
"attributes-name": { "attributes-name": {
"min-period": "Minimale Periode", "min-period": "Minimale Periode",
"max-period": "Maximale Periode", "max-period": "Maximale Periode",
@ -2810,20 +2829,20 @@
"server-attributes-placeholder": "Server-Attribute", "server-attributes-placeholder": "Server-Attribute",
"timeseries-placeholder": "Zeitreihen", "timeseries-placeholder": "Zeitreihen",
"target-entity": "Zielentität", "target-entity": "Zielentität",
"attributes-propagation": "Attributübertragung", "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 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-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": "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.", "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ätsansichten suchen", "search": "Entitätssichten suchen",
"selected-entity-views": "{ count, plural, =1 {1 Entitätsansicht} other {# Entitätsansichten} } ausgewählt", "selected-entity-views": "{ count, plural, =1 {1 Entitätssicht} other {# Entitätssichten} } ausgewählt",
"assign-entity-view-to-edge": "Entitätsansicht(en) Edge zuweisen", "assign-entity-view-to-edge": "Entitätssicht(en) einer Edge zuweisen",
"assign-entity-view-to-edge-text": "Bitte wählen Sie die Entitätsansichten aus, die dem Edge zugewiesen werden sollen", "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": "Sind Sie sicher, dass Sie die Entitätsansicht '{{entityViewName}}' von Edge entfernen möchten?", "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 von Edge entfernt und ist nicht mehr zugänglich.", "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} other {# Entitätsansichten} } von Edge entfernen", "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 von Edge entfernen", "unassign-entity-view-from-edge": "Entitätsansicht 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-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ätsansichten von Edge entfernt und sind nicht mehr zugänglich." "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": {
"event-type": "Ereignistyp", "event-type": "Ereignistyp",
@ -3041,15 +3060,15 @@
"edit": "Filter bearbeiten", "edit": "Filter bearbeiten",
"name": "Filtername", "name": "Filtername",
"name-required": "Filtername ist erforderlich.", "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", "filters": "Filter",
"unable-delete-filter-title": "Filter kann nicht gelöscht werden", "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}}", "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.", "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}}'.", "missing-key-filters-error": "Schlüsselfilter fehlt für Filter '{{filter}}'.",
"filter": "Filter", "filter": "Filter",
"editable": "Bearbeitbar", "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-filters-found": "Keine Filter gefunden.",
"no-filter-text": "Kein Filter angegeben", "no-filter-text": "Kein Filter angegeben",
"add-filter-prompt": "Bitte Filter hinzufügen", "add-filter-prompt": "Bitte Filter hinzufügen",
@ -4303,7 +4322,7 @@
}, },
"profile": { "profile": {
"profile": "Profil", "profile": "Profil",
"last-login-time": "Letzte Anmeldung", "last-login-time": "Letzter Login",
"change-password": "Passwort ändern", "change-password": "Passwort ändern",
"current-password": "Aktuelles Passwort", "current-password": "Aktuelles Passwort",
"copy-jwt-token": "JWT-Token kopieren", "copy-jwt-token": "JWT-Token kopieren",
@ -4478,7 +4497,8 @@
"jks": "JKS", "jks": "JKS",
"js-module": "JS-Modul", "js-module": "JS-Modul",
"lwm2m-model": "LWM2M-Modell", "lwm2m-model": "LWM2M-Modell",
"pkcs-12": "PKCS #12" "pkcs-12": "PKCS #12",
"general": "Allgemein"
}, },
"resource-sub-type": "Subtyp", "resource-sub-type": "Subtyp",
"sub-type": { "sub-type": {
@ -4486,7 +4506,12 @@
"scada-symbol": "SCADA-Symbol", "scada-symbol": "SCADA-Symbol",
"extension": "Erweiterung", "extension": "Erweiterung",
"module": "Modul" "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": { "javascript": {
"add": "JavaScript-Ressource hinzufügen", "add": "JavaScript-Ressource hinzufügen",
@ -4742,24 +4767,24 @@
"send-attributes-deleted-notification": "Benachrichtigung bei gelöschten Attributen senden", "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.", "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": "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": "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 Attribute nur bei Wertänderung. Keine Aktualisierung von Zeitstempel oder Änderungsbenachrichtigung bei gleichem Wert.", "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", "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-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, erzwingt Benachrichtigung an das Gerät bei Attribut-Entfernung. Bei Deaktivierung erfolgt Steuerung über Metadatenparameter 'notifyDevice'.", "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 Zeitreihenschlüssel", "latest-timeseries": "Neueste Zeitreihendatenschlüssel",
"timeseries-keys": "Zeitreihenschlü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-timeseries-key": "Zeitreihenschlüssel hinzufügen",
"add-message-field": "Nachrichtenfeld hinzufügen", "add-message-field": "Nachrichtenfeld hinzufügen",
"relation-search-parameters": "Beziehungs-Suchparameter", "relation-search-parameters": "Suchparameter für Beziehungen",
"relation-parameters": "Beziehungsparameter", "relation-parameters": "Beziehungsparameter",
"add-metadata-field": "Metadatenfeld hinzufügen", "add-metadata-field": "Metadatenfeld hinzufügen",
"data-keys": "Feldnamen der Nachricht", "data-keys": "Feldnamen der Nachricht",
"copy-from": "Kopieren von", "copy-from": "Kopieren von",
"data-to-metadata": "Daten zu Metadaten", "data-to-metadata": "Daten zu Metadaten",
"metadata-to-data": "Metadaten zu Daten", "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": "Intervall",
"interval-required": "Intervall ist erforderlich", "interval-required": "Intervall ist erforderlich",
"interval-hint": "Deduplizierungsintervall in Sekunden.", "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.", "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", "relations-query-config-direction-suffix": "Absender",
"profile-name": "Profilname", "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-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}}' sollte folgendes Format haben: [[48.19736,24.65235],[48.19800,24.65060],...,[48.19849,24.65420]]", "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] für Nachrichtenwerte und ${metadataKey} für Metadatenwerte.", "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.", "fields-mapping-required": "Mindestens eine Feldzuordnung muss angegeben werden.",
"at-least-one-field-required": "Mindestens ein Eingabefeld muss einen Wert enthalten.", "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}.", "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] oder ${metadataKey}.", "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": "Quellfeld",
"source-field-required": "Quellfeld ist erforderlich.", "source-field-required": "Quellfeld ist erforderlich.",
"originator-source": "Absenderquelle", "originator-source": "Absenderquelle",
@ -5436,11 +5461,11 @@
"prompt-settings": "Prompt-Einstellungen", "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.", "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": "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.", "system-prompt-blank": "Der System-Prompt darf nicht leer sein.",
"user-prompt": "Benutzer-Prompt", "user-prompt": "Benutzer-Prompt",
"user-prompt-required": "Benutzer-Prompt ist erforderlich.", "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.", "user-prompt-blank": "Der Benutzer-Prompt darf nicht leer sein.",
"response-format": "Antwortformat", "response-format": "Antwortformat",
"response-text": "Text", "response-text": "Text",
@ -5457,7 +5482,8 @@
"timeout-required": "Zeitüberschreitung ist erforderlich", "timeout-required": "Zeitüberschreitung ist erforderlich",
"timeout-validation": "Muss zwischen 1 Sekunde und 10 Minuten liegen.", "timeout-validation": "Muss zwischen 1 Sekunde und 10 Minuten liegen.",
"force-acknowledgement": "Erzwinge Bestätigung", "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": { "timezone": {
@ -5987,9 +6013,10 @@
"label": "Beschriftung", "label": "Beschriftung",
"value": "Wert", "value": "Wert",
"date": "Datum", "date": "Datum",
"show-date-time-interval": "Datum/Zeit-Intervall anzeigen", "show-date-time-interval": "Datums-/Zeitintervall anzeigen",
"show-date-time-interval-hint": "Datum/Zeit-Intervall gemäß Datenaggregation anzeigen.", "show-date-time-interval-hint": "Zeigt das Datums-/Zeitintervall entsprechend der Datenaggregation an.",
"hide-zero-tooltip-values": "Nullwerte ausblenden", "hide-zero-tooltip-values": "Nullwerte ausblenden",
"show-stack-total": "Gesamtwert im Stapelmodus anzeigen",
"background-color": "Hintergrundfarbe", "background-color": "Hintergrundfarbe",
"background-blur": "Hintergrundunschärfe" "background-blur": "Hintergrundunschärfe"
}, },
@ -6556,10 +6583,10 @@
"no-users-text": "Keine Benutzer gefunden", "no-users-text": "Keine Benutzer gefunden",
"user-details": "Benutzerdetails", "user-details": "Benutzerdetails",
"delete-user-title": "Möchten Sie den Benutzer '{{userEmail}}' wirklich löschen?", "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-user-text": "Achtung: 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-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-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!", "activation-email-sent-message": "Aktivierungs-E-Mail wurde erfolgreich gesendet!",
"resend-activation": "Aktivierung erneut senden", "resend-activation": "Aktivierung erneut senden",
"email": "E-Mail", "email": "E-Mail",
@ -6669,24 +6696,24 @@
"remove-all": "Alle entfernen", "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.", "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", "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", "restore-entities-from-version": "Entitäten aus Version '{{versionName}}' wiederherstellen",
"restoring-entities-from-version": "Entitäten werden wiederhergestellt... Bitte warten", "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", "created": "{{created}} erstellt",
"updated": "{{updated}} aktualisiert", "updated": "{{updated}} aktualisiert",
"deleted": "{{deleted}} gelöscht", "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.", "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": "automatisch in <b>{{ branch }}</b>-Zweig übernehmen", "auto-commit-to-branch": "Auto-Commit in den Branch <b>{{branch}}</b>",
"default-create-entity-version-name": "{{entityName}} Aktualisierung", "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-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 ausgewählte Entitäten im Repository. Alle anderen Repository-Entitäten werden <b>gelöscht</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": "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.", "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": "<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.", "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}}", "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.", "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": "Bei Fehler zurücksetzen", "rollback-on-error": "Rollback bei Fehlern",
"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." "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": {
"widget-library": "Widget-Bibliothek", "widget-library": "Widget-Bibliothek",
@ -6703,8 +6730,8 @@
"management": "Widget-Verwaltung", "management": "Widget-Verwaltung",
"editor": "Widget-Editor", "editor": "Widget-Editor",
"confirm-to-exit-editor-html": "Sie haben nicht gespeicherte Widget-Einstellungen.<br>Möchten Sie diese Seite wirklich verlassen?", "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-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 folgender Fehler nicht geladen werden:", "widget-type-load-error": "Widget konnte aufgrund der folgenden Fehler nicht geladen werden:",
"remove": "Widget entfernen", "remove": "Widget entfernen",
"delete": "Widget löschen", "delete": "Widget löschen",
"edit": "Widget bearbeiten", "edit": "Widget bearbeiten",
@ -6769,11 +6796,11 @@
"javascript": "JavaScript", "javascript": "JavaScript",
"js": "JS", "js": "JS",
"delete-widget-title": "Möchten Sie das Widget '{{widgetName}}' wirklich löschen?", "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-widget-text": "Nach der Bestätigung wird das Widget und alle zugehörigen Daten unwiderruflich gelöscht.",
"delete-widgets-title": "Möchten Sie wirklich { count, plural, =1 {1 Widget} other {# Widgets} } löschen?", "delete-widgets-title": "Möchten Sie { count, plural, =1 {1 Widget} other {# Widgets} } wirklich löschen?",
"delete-widgets-text": "Nach der Bestätigung werden alle ausgewählten Widgets und alle zugehörigen Daten gelöscht.", "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", "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", "details": "Details",
"widget-details": "Widget-Details", "widget-details": "Widget-Details",
"add": "Widget hinzufügen", "add": "Widget hinzufügen",
@ -8572,15 +8599,15 @@
"fit-map-bounds": "Kartenausschnitt anpassen, um alle Marker abzudecken", "fit-map-bounds": "Kartenausschnitt anpassen, um alle Marker abzudecken",
"use-default-map-center-position": "Standard-Zentrum der Karte verwenden", "use-default-map-center-position": "Standard-Zentrum der Karte verwenden",
"entities-limit": "Grenze für zu ladende Entitäten", "entities-limit": "Grenze für zu ladende Entitäten",
"markers-settings": "Marker-Einstellungen", "markers-settings": "Markereinstellungen",
"marker-offset-x": "X-Versatz des Markers multipliziert mit Breite", "marker-offset-x": "Marker-X-Versatz relativ zur Position multipliziert mit der Markerbreite",
"marker-offset-y": "Y-Versatz des Markers multipliziert mit Höhe", "marker-offset-y": "Marker-Y-Versatz relativ zur Position multipliziert mit der Markerhöhe",
"position-function": "Positionsumrechnungsfunktion, sollte x,y-Koordinaten als Double (0-1) zurückgeben", "position-function": "Positionskonvertierungsfunktion, sollte x,y-Koordinaten im Bereich von 0 bis 1 (double) zurückgeben",
"draggable-marker": "Marker verschiebbar", "draggable-marker": "Verschiebbarer Marker",
"label": "Beschriftung", "label": "Beschriftung",
"show-label": "Beschriftung anzeigen", "show-label": "Beschriftung anzeigen",
"use-label-function": "Beschriftungsfunktion verwenden", "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", "label-function": "Beschriftungsfunktion",
"tooltip": "Tooltip", "tooltip": "Tooltip",
"show-tooltip": "Tooltip anzeigen", "show-tooltip": "Tooltip anzeigen",
@ -9522,4 +9549,4 @@
"language": { "language": {
"language": "Sprache" "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": {
"timewindow": "Time window", "timewindow": "Time window",
"timewindow-settings": "Time window settings", "timewindow-settings": "Time window settings",
"years": "{ years, plural, =1 { year } other {# years } }", "years": "{ years, plural, =1 {1 year } other {# years } }",
"years-short": "{{ years }}y", "years-short": "{{ years }}y",
"months": "{ months, plural, =1 { month } other {# months } }", "months": "{ months, plural, =1 {1 month } other {# months } }",
"months-short": "{{ months }}M", "months-short": "{{ months }}M",
"weeks": "{ weeks, plural, =1 { week } other {# weeks } }", "weeks": "{ weeks, plural, =1 {1 week } other {# weeks } }",
"weeks-short": "{{ weeks }}w", "weeks-short": "{{ weeks }}w",
"days": "{ days, plural, =1 { day } other {# days } }", "days": "{ days, plural, =1 {1 day } other {# days } }",
"days-short": "{{ days }}d", "days-short": "{{ days }}d",
"hours": "{ hours, plural, =0 { hour } =1 {1 hour } other {# hours } }", "hours": "{ hours, plural, =0 { hour } =1 {1 hour } other {# hours } }",
"hr": "{{ hr }} hr", "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: dependencies:
minipass "^7.0.3" 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: fsevents@^2.3.2, fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3" version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" 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" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@^10.2.2, glob@^10.3.10, glob@^10.3.3, glob@^10.3.7: 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.4.5" version "10.5.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c"
integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==
dependencies: dependencies:
foreground-child "^3.1.0" foreground-child "^3.1.0"
jackspeak "^3.1.2" 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" package-json-from-dist "^1.0.0"
path-scurry "^1.11.1" 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: global-prefix@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-4.0.0.tgz#e9cc79aab9be1d03287e156a3f912dd0895463ed" 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" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== 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: inherits@2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== 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: ini@4.1.3, ini@^4.1.3:
version "4.1.3" version "4.1.3"
resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.3.tgz#4c359675a6071a46985eb39b14e4a2c0ec98a795" resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.3.tgz#4c359675a6071a46985eb39b14e4a2c0ec98a795"
@ -7170,7 +7145,7 @@ minimatch@9.0.1:
dependencies: dependencies:
brace-expansion "^2.0.1" brace-expansion "^2.0.1"
minimatch@^3.1.1, minimatch@^3.1.2: minimatch@^3.1.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@ -7493,10 +7468,10 @@ node-fetch@^3.3.2:
fetch-blob "^3.1.4" fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10" formdata-polyfill "^4.0.10"
node-forge@^1: node-forge@1.3.3, node-forge@^1:
version "1.3.1" version "1.3.3"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751"
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==
node-gyp-build-optional-packages@5.2.2: node-gyp-build-optional-packages@5.2.2:
version "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" resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== 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: onetime@^5.1.0, onetime@^5.1.2:
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" 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" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7"
integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== 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: path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" 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" lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
path-to-regexp@0.1.10: path-to-regexp@0.1.10, path-to-regexp@0.1.12:
version "0.1.10" version "0.1.12"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
path-type@^5.0.0: path-type@^5.0.0:
version "5.0.0" version "5.0.0"
@ -10079,11 +10042,6 @@ wrap-ansi@^9.0.0:
string-width "^7.0.0" string-width "^7.0.0"
strip-ansi "^7.1.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: ws@^8.16.0:
version "8.18.0" version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"

Loading…
Cancel
Save