diff --git a/application/pom.xml b/application/pom.xml index 4cbd9c3b64..86d50604e7 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -382,35 +382,35 @@ rocksdbjni - dev.langchain4j + org.thingsboard.langchain4j langchain4j-open-ai - dev.langchain4j + org.thingsboard.langchain4j langchain4j-azure-open-ai - dev.langchain4j + org.thingsboard.langchain4j langchain4j-google-ai-gemini - dev.langchain4j + org.thingsboard.langchain4j langchain4j-vertex-ai-gemini - dev.langchain4j + org.thingsboard.langchain4j langchain4j-mistral-ai - dev.langchain4j + org.thingsboard.langchain4j langchain4j-anthropic - dev.langchain4j + org.thingsboard.langchain4j langchain4j-bedrock - dev.langchain4j + org.thingsboard.langchain4j langchain4j-github-models @@ -420,7 +420,7 @@ - dev.langchain4j + org.thingsboard.langchain4j langchain4j-ollama diff --git a/application/src/main/data/json/edge/instructions/install/centos/instructions.md b/application/src/main/data/json/edge/instructions/install/centos/instructions.md index de5e57d926..1822add6d8 100644 --- a/application/src/main/data/json/edge/instructions/install/centos/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/centos/instructions.md @@ -1,7 +1,7 @@ -Here is the list of commands, that can be used to quickly install ThingsBoard Edge on RHEL/CentOS 7/8 and connect to the server. +Here is the list of commands that can be used to quickly install ThingsBoard Edge on RHEL/CentOS 7/8 and connect to the server. #### Prerequisites -Before continue to installation execute the following commands in order to install necessary tools: +Before continuing to installation, execute the following commands to install the necessary tools: ```bash sudo yum install -y nano wget && sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm @@ -9,29 +9,28 @@ sudo yum install -y nano wget && sudo yum install -y https://dl.fedoraproject.or ``` #### Step 1. Install Java 17 (OpenJDK) -ThingsBoard service is running on Java 17. Follow these instructions to install OpenJDK 17: +ThingsBoard service is running on Java 17. To install OpenJDK 17, follow these instructions: ```bash sudo dnf install java-17-openjdk {:copy-code} ``` -Please don't forget to configure your operating system to use OpenJDK 17 by default. -You can configure which version is the default using the following command: +Configure your operating system to use OpenJDK 17 by default. You can configure the default version by running the following command: ```bash sudo update-alternatives --config java {:copy-code} ``` -You can check the installation using the following command: +To check the installed Java version on your system, use the following command: ```bash java -version {:copy-code} ``` -Expected command output is: +The expected result is: ```text openjdk version "17.x.xx" @@ -39,14 +38,13 @@ OpenJDK Runtime Environment (...) OpenJDK 64-Bit Server VM (build ...) ``` -#### Step 2. Configure ThingsBoard Database -ThingsBoard Edge supports SQL and hybrid database approaches. -In this guide we will use SQL only. -For hybrid details please follow official installation instructions from the ThingsBoard documentation site. +#### Step 2. Configure ThingsBoard Edge Database -### PostgresSql -ThingsBoard Edge uses PostgreSQL database as a local storage. -To install PostgreSQL, follow the instructions below. +ThingsBoard Edge supports **SQL** and **hybrid** database configurations. +In this guide, we’ll use an **SQL** database. +For more details about the hybrid setup, please refer to the official installation instructions on the ThingsBoard documentation site. + +To install the PostgreSQL database, run these commands: ```bash # Update your system @@ -55,7 +53,8 @@ sudo dnf update ``` Install the repository RPM: -**For CentOS/RHEL 8:** + +* **For CentOS/RHEL 8:** ```bash # Install the repository RPM (For CentOS/RHEL 8): @@ -63,7 +62,7 @@ sudo sudo dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/ {:copy-code} ``` -**For CentOS/RHEL 9:** +* **For CentOS/RHEL 9:** ```bash # Install the repository RPM (for CentOS 9): @@ -111,22 +110,11 @@ sudo systemctl restart postgresql-16.service && psql -U postgres -d postgres -h {:copy-code} ``` -#### Step 3. Choose Queue Service - -ThingsBoard Edge supports only Kafka or in-memory queue (since v4.0) for message storage and communication between ThingsBoard services. -How to choose the right queue implementation? - -In Memory queue implementation is built-in and default. It is useful for development(PoC) environments and is not suitable for production deployments or any sort of cluster deployments. - -Kafka is recommended for production deployments. This queue is used on the most of ThingsBoard production environments now. - -In Memory queue is built in and enabled by default. No additional configuration is required. - -#### Step 4. ThingsBoard Edge Service Installation +#### Step 3. ThingsBoard Edge Service Installation Download installation package: ```bash -wget wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.rpm +wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.rpm {:copy-code} ``` @@ -137,8 +125,8 @@ sudo rpm -Uvh tb-edge-${TB_EDGE_TAG}.rpm {:copy-code} ``` -#### Step 5. Configure ThingsBoard Edge -To configure ThingsBoard Edge, you can use the following command to automatically update the configuration file with specific values: +#### Step 4. Configure ThingsBoard Edge +To configure ThingsBoard Edge, you can use the following command to automatically update the configuration file with specific values: ```bash sudo sh -c 'cat <> /etc/tb-edge/conf/tb-edge.conf @@ -151,7 +139,7 @@ EOL' {:copy-code} ``` -##### Configure PostgreSQL (Optional) +##### [Optional] Configure PostgreSQL Connection If you changed PostgreSQL default datasource settings, use the following command: ```bash @@ -163,11 +151,10 @@ EOL' {:copy-code} ``` -PUT_YOUR_POSTGRESQL_PASSWORD_HERE: Replace with your actual PostgreSQL user password. - +* **PUT_YOUR_POSTGRESQL_PASSWORD_HERE**: Replace with your actual **PostgreSQL user password**. -##### [Optional] Update bind ports -If ThingsBoard Edge is going to be running on the same machine where ThingsBoard server (cloud) is running, you'll need to update configuration parameters to avoid port collision between ThingsBoard server and ThingsBoard Edge. +##### [Optional] Update Bind Ports +If ThingsBoard Edge runs on the same machine as the ThingsBoard Server, you need to update the port configuration to avoid conflicts between the two services. Please execute the following command to update ThingsBoard Edge configuration file (**/etc/tb-edge/conf/tb-edge.conf**): @@ -182,28 +169,26 @@ EOL' {:copy-code} ``` -Make sure that ports above (18080, 11883, 15683) are not used by any other application. +Make sure that ports **18080**, **11883**, and **15683–15688** are not being used by any other applications. + +#### Step 5. Run Installation Script -#### Step 6. Run installation Script -Once ThingsBoard Edge is installed and configured please execute the following install script: +Once ThingsBoard Edge is installed and configured, please execute the following install script: ```bash sudo /usr/share/tb-edge/bin/install/install.sh {:copy-code} ``` -#### Step 7. Restart ThingsBoard Edge Service +#### Step 6. Start ThingsBoard Edge Service ```bash -sudo service tb-edge restart +sudo service tb-edge start {:copy-code} ``` -#### Step 8. Open ThingsBoard Edge UI - -Once started, you will be able to open **ThingsBoard Edge UI** using the following link http://localhost:8080. - -###### NOTE: Edge HTTP bind port update +#### Step 7. Open ThingsBoard Edge UI -If the Edge HTTP bind port was changed to 18080 during Edge installation, access the ThingsBoard Edge instance at http://localhost:18080. +Once the Edge service has started, open the Edge web interface at http://localhost:8080, or http://localhost:18080 if you modified the HTTP bind port configuration in the previous step. +Log in using your **tenant credentials** from either your local ThingsBoard Server or the **ThingsBoard Live Demo**. \ No newline at end of file diff --git a/application/src/main/data/json/edge/instructions/install/docker/instructions.md b/application/src/main/data/json/edge/instructions/install/docker/instructions.md index e568d273af..8325c006d3 100644 --- a/application/src/main/data/json/edge/instructions/install/docker/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/docker/instructions.md @@ -1,34 +1,17 @@ -Here is the list of commands, that can be used to quickly install ThingsBoard Edge using docker compose and connect to the server. +Here is the list of commands that can be used to quickly install ThingsBoard Edge using docker compose and connect to the server. #### Prerequisites Install Docker CE and Docker Compose. -#### 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 ThingsBoard documentation site. - thingsboard/tb-edge - -#### Step 2. Choose Queue and/or Database Services - -ThingsBoard Edge supports only Kafka or in-memory queue (since v4.0) for message storage and communication between ThingsBoard services. - -ThingsBoard Edge supports SQL and hybrid database approaches. -In this guide we will use SQL only. -For hybrid details please follow official installation instructions from the ThingsBoard documentation site. - -How to choose the right queue implementation? - -In Memory queue implementation is built-in and default. It is useful for development(PoC) environments and is not suitable for production deployments or any sort of cluster deployments. - -Kafka is recommended for production deployments. This queue is used on the most of ThingsBoard production environments now. - -Hybrid implementation combines PostgreSQL and Cassandra databases with Kafka queue service. It is recommended if you plan to manage 1M+ devices in production or handle high data ingestion rate (more than 5000 msg/sec). - -Create a docker compose file for the ThingsBoard Edge service: - -##### In Memory +Now, create a Docker Compose file for the ThingsBoard Edge service: ```bash nano docker-compose.yml @@ -38,7 +21,6 @@ nano docker-compose.yml Add the following lines to the yml file: ```bash -version: '3.8' services: mytbedge: restart: always @@ -54,6 +36,7 @@ services: CLOUD_RPC_HOST: ${BASE_URL} CLOUD_RPC_PORT: ${CLOUD_RPC_PORT} CLOUD_RPC_SSL_ENABLED: ${CLOUD_RPC_SSL_ENABLED} + ${EXTRA_HOSTS} volumes: - tb-edge-data:/data - tb-edge-logs:/var/log/tb-edge @@ -78,41 +61,28 @@ volumes: {:copy-code} ``` -##### [Optional] Update bind ports -If ThingsBoard Edge is set to run on the same machine where the ThingsBoard server is operating, you need to update port configuration to prevent port collision between the ThingsBoard server and ThingsBoard Edge. +##### [Optional] Update Bind Ports +If ThingsBoard Edge runs on the same machine as the ThingsBoard Server, you need to update the port configuration to avoid conflicts between the two services. -Ensure that the ports 18080, 11883, 15683-15688 are not used by any other application. +Make sure that ports **18080**, **11883**, and **15683–15688** are not being used by any other applications. -Then, update the port configuration in the docker-compose.yml file: -```bash -sed -i ‘s/8080:8080/18080:8080/; s/1883:1883/11883:1883/; s/5683-5688:5683-5688\/udp/15683-15688:5683-5688\/udp/’ docker-compose.yml -{:copy-code} -``` -#### Start ThingsBoard Edge -Set the terminal in the directory which contains the docker-compose.yml file and execute the following commands to up this docker compose directly: +Then, update the port configuration in the `docker-compose.yml` file accordingly: ```bash -docker compose up -d && docker compose logs -f mytbedge +sed -i 's/8080:8080/18080:8080/; s/1883:1883/11883:1883/; s/5683-5688:5683-5688\/udp/15683-15688:5683-5688\/udp/' docker-compose.yml {:copy-code} ``` -###### NOTE: Docker Compose V2 vs docker-compose (with a hyphen) - -ThingsBoard supports Docker Compose V2 (Docker Desktop or Compose plugin) starting from **3.4.2** release, because **docker-compose** as standalone setup is no longer supported by Docker. -We **strongly** recommend to update to Docker Compose V2 and use it. -If you still rely on using Docker Compose as docker-compose (with a hyphen), then please execute the following commands to start ThingsBoard Edge: +#### Step 2. Start ThingsBoard Edge +Navigate to the directory containing the `docker-compose.yml` file and run the following command to start the ThingsBoard Edge service: ```bash -docker-compose up -d -docker-compose logs -f mytbedge +docker compose up -d && docker compose logs -f mytbedge +{:copy-code} ``` #### Step 3. Open ThingsBoard Edge UI -Once the Edge service is started, open the Edge UI at http://localhost:8080. - -###### NOTE: Edge HTTP bind port update - -If the Edge HTTP bind port was changed to 18080 during Edge installation, access the ThingsBoard Edge instance at http://localhost:18080. +Once the Edge service has started, open the Edge web interface at http://localhost:8080, or http://localhost:18080 if you modified the HTTP bind port configuration in the previous step. -Please use your tenant credentials from local Server instance or ThingsBoard Live Demo to log in to the ThingsBoard Edge. +Log in using your **tenant credentials** from either your local ThingsBoard Server or the **ThingsBoard Live Demo**. diff --git a/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md b/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md index a56b83e558..b4782dd46a 100644 --- a/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md @@ -1,4 +1,4 @@ -Here is the list of commands, that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the server. +Here is the list of commands that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the server. #### Step 1. Install Java 17 (OpenJDK) ThingsBoard service is running on Java 17. To install OpenJDK 17, follow these instructions: @@ -25,19 +25,16 @@ java -version The expected result is: ```text -openjdk version "17.x.xx" +openjdk version "17.x.xx" OpenJDK Runtime Environment (...) -OpenJDK 64-Bit Server VM (...) +OpenJDK 64-Bit Server VM (build ...) ``` #### Step 2. Configure ThingsBoard Edge Database -ThingsBoard Edge supports SQL and hybrid database approaches. -In this guide we will use SQL only. -For hybrid details please follow official installation instructions from the ThingsBoard documentation site. - -### Configure PostgreSQL -ThingsBoard Edge uses PostgreSQL database as a local storage. +ThingsBoard Edge supports **SQL** and **hybrid** database configurations. +In this guide, we’ll use an **SQL** database. +For more details about the hybrid setup, please refer to the official installation instructions on the ThingsBoard documentation site. To install the PostgreSQL database, run these commands: @@ -71,18 +68,8 @@ echo "CREATE DATABASE tb_edge;" | psql -U postgres -d postgres -h 127.0.0.1 -W {:copy-code} ``` -#### Step 3. Choose Queue Service - -ThingsBoard Edge supports only Kafka or in-memory queue (since v4.0) for message storage and communication between ThingsBoard services. Choose the appropriate queue implementation based on your specific business needs: - -In Memory: The built-in and default queue implementation. It is useful for development or proof-of-concept (PoC) environments, but is not recommended for production or any type of clustered deployments due to limited scalability. - -Kafka: Recommended for production deployments. This queue is used in the most of ThingsBoard production environments now. - -In Memory queue is built in and enabled by default. No additional configuration is required. - -#### Step 4. ThingsBoard Edge Service Installation -Download installation package: +#### Step 3. ThingsBoard Edge Service Installation +Download the installation package: ```bash wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.deb @@ -96,8 +83,8 @@ sudo dpkg -i tb-edge-${TB_EDGE_TAG}.deb {:copy-code} ``` -#### Step 5. Configure ThingsBoard Edge -To configure ThingsBoard Edge, you can use the following command to automatically update the configuration file with specific values: +#### Step 4. Configure ThingsBoard Edge +To configure ThingsBoard Edge, you can use the following command to automatically update the configuration file with specific values: ```bash sudo sh -c 'cat <> /etc/tb-edge/conf/tb-edge.conf @@ -110,7 +97,7 @@ EOL' {:copy-code} ``` -##### [Optional] Configure PostgreSQL +##### [Optional] Configure PostgreSQL Connection If you changed PostgreSQL default datasource settings, use the following command: ```bash @@ -122,10 +109,10 @@ EOL' {:copy-code} ``` -PUT_YOUR_POSTGRESQL_PASSWORD_HERE: Replace with your actual PostgreSQL user password. +* **PUT_YOUR_POSTGRESQL_PASSWORD_HERE**: Replace with your actual **PostgreSQL user password**. -##### [Optional] Update bind ports -If ThingsBoard Edge is going to be running on the same machine where ThingsBoard server (cloud) is running, you'll need to update configuration parameters to avoid port collision between ThingsBoard server and ThingsBoard Edge. +##### [Optional] Update Bind Ports +If ThingsBoard Edge runs on the same machine as the ThingsBoard Server, you need to update the port configuration to avoid conflicts between the two services. Please execute the following command to update ThingsBoard Edge configuration file (**/etc/tb-edge/conf/tb-edge.conf**): @@ -140,29 +127,26 @@ EOL' {:copy-code} ``` -Make sure that ports above (18080, 11883, 15683) are not used by any other application. +Make sure that ports **18080**, **11883**, and **15683–15688** are not being used by any other applications. -#### Step 6. Run installation Script +#### Step 5. Run Installation Script -Once ThingsBoard Edge is installed and configured please execute the following install script: +Once ThingsBoard Edge is installed and configured, please execute the following installation script: ```bash sudo /usr/share/tb-edge/bin/install/install.sh {:copy-code} ``` -#### Step 7. Restart ThingsBoard Edge Service +#### Step 6. Start ThingsBoard Edge Service ```bash -sudo service tb-edge restart +sudo service tb-edge start {:copy-code} ``` -#### Step 8. Open ThingsBoard Edge UI - -Once started, you will be able to open **ThingsBoard Edge UI** using the following link http://localhost:8080. - -###### NOTE: Edge HTTP bind port update +#### Step 7. Open ThingsBoard Edge UI -Use next **ThingsBoard Edge UI** link **http://localhost:18080** if you updated HTTP 8080 bind port to **18080**. +Once the Edge service has started, open the Edge web interface at http://localhost:8080, or http://localhost:18080 if you modified the HTTP bind port configuration in the previous step. +Log in using your **tenant credentials** from either your local ThingsBoard Server or the **ThingsBoard Live Demo**. \ No newline at end of file diff --git a/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md b/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md index c0e721ace2..82774e89cb 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md +++ b/application/src/main/data/json/edge/instructions/upgrade/centos/instructions.md @@ -1,15 +1,15 @@ #### Upgrading to ${TB_EDGE_VERSION}EDGE -**ThingsBoard Edge package download:** +ThingsBoard Edge package download: ```bash wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.rpm {:copy-code} ``` -##### ThingsBoard Edge service upgrade Install package: ```bash sudo rpm -Uvh tb-edge-${TB_EDGE_TAG}.rpm {:copy-code} ``` + ${UPGRADE_DB} diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md b/application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md index f1b9931e8b..68c7597f84 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/instructions.md @@ -1,10 +1,3 @@ #### Upgrading to ${TB_EDGE_VERSION} -Execute the following command to pull **${TB_EDGE_VERSION}** image: - -```bash -docker pull thingsboard/tb-edge:${TB_EDGE_VERSION} -{:copy-code} -``` - ${UPGRADE_DB} diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md b/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md index a6c9c6731c..11dc99a637 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/start_service.md @@ -1,11 +1,10 @@ -Modify ‘main’ docker compose (`docker-compose.yml`) file for ThingsBoard Edge and update version of the image: +Modify ‘main’ docker compose (`docker-compose.yml`) a file for ThingsBoard Edge and update a version of the image: ```bash nano docker-compose.yml {:copy-code} ``` ```text -version: '3.8' services: mytbedge: restart: always @@ -13,7 +12,7 @@ services: ... ``` -Make sure your image is the set to **tb-edge-${TB_EDGE_VERSION}**. +Make sure your image is set to **tb-edge:${TB_EDGE_VERSION}**. Execute the following commands to up this docker compose directly: ```bash diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md index 386c5f2743..523316cdae 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md @@ -1,4 +1,4 @@ -Create docker compose file for ThingsBoard Edge upgrade process: +Create a docker compose file for ThingsBoard Edge upgrade process: ```bash > docker-compose-upgrade.yml && nano docker-compose-upgrade.yml @@ -8,7 +8,6 @@ Create docker compose file for ThingsBoard Edge upgrade process: Add the following lines to the yml file: ```bash -version: '3.8' services: mytbedge: restart: on-failure @@ -40,7 +39,7 @@ volumes: {:copy-code} ``` -Execute the following command to start upgrade process: +Execute the following command to start an upgrade process: ```bash docker compose -f docker-compose-upgrade.yml up --abort-on-container-exit diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md index f12eaa5fc0..d2a6f796a1 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md @@ -1,6 +1,6 @@ -Here is the list of commands, that can be used to quickly upgrade ThingsBoard Edge on Docker (Linux or MacOS). +Here is the list of commands that can be used to quickly upgrade ThingsBoard Edge on Docker (Linux or macOS). -#### Prepare for upgrading ThingsBoard Edge +#### Prepare for Upgrading ThingsBoard Edge Set the terminal in the directory which contains the `docker-compose.yml` file and execute the following command to stop and remove currently running TB Edge container: @@ -10,70 +10,6 @@ docker compose rm mytbedge {:copy-code} ``` -**OPTIONAL:** If you still rely on Docker Compose as docker-compose (with a hyphen) here is the list of the above commands: -```text -docker-compose stop -docker-compose rm mytbedge -``` - -##### Migrating Data from Docker Bind Mount Folders to Docker Volumes -Starting with the **3.6.2** release, the ThingsBoard team has transitioned from using Docker bind mount folders to Docker volumes. -This change aims to enhance security and efficiency in storing data for Docker containers and to mitigate permission issues across various environments. - -To migrate from Docker bind mounts to Docker volumes, please execute the following commands: - -```bash -docker run --rm -v tb-edge-data:/volume -v ~/.mytb-edge-data:/backup busybox sh -c "cp -a /backup/. /volume" -docker run --rm -v tb-edge-logs:/volume -v ~/.mytb-edge-logs:/backup busybox sh -c "cp -a /backup/. /volume" -docker run --rm -v tb-edge-postgres-data:/volume -v ~/.mytb-edge-data/db:/backup busybox sh -c "cp -a /backup/. /volume" -{:copy-code} -``` - -After completing the data migration to the newly created Docker volumes, you'll need to update the volume mounts in your Docker Compose configuration. -Modify the `docker-compose.yml` file for ThingsBoard Edge to update the volume settings. - -Update volume mounts. Locate the following snippet: -```text - volumes: - - ~/.mytb-edge-data:/data - - ~/.mytb-edge-logs:/var/log/tb-edge -... -``` - -And replace it with: -```text - volumes: - - tb-edge-data:/data - - tb-edge-logs:/var/log/tb-edge -... -``` - -Apply a similar update for the PostgreSQL service. Find the section: -```text - volumes: - - ~/.mytb-edge-data/db:/var/lib/postgresql/data -... -``` - -And replace it with: -```text - volumes: - - tb-edge-postgres-data:/var/lib/postgresql/data -... -``` - -Finally, please add next volumes section at the end of the file: -```text -... -volumes: - tb-edge-data: - name: tb-edge-data - tb-edge-logs: - name: tb-edge-logs - tb-edge-postgres-data: - name: tb-edge-postgres-data -``` - ##### Backup Database Make a copy of the database volume before upgrading: diff --git a/application/src/main/data/json/edge/instructions/upgrade/start_service.md b/application/src/main/data/json/edge/instructions/upgrade/start_service.md index c26c183154..109ccf5b4b 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/start_service.md +++ b/application/src/main/data/json/edge/instructions/upgrade/start_service.md @@ -1,4 +1,4 @@ -Start the service +#### Start the Service ```bash sudo systemctl tb-edge start diff --git a/application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md b/application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md index 59d243c63c..8d4a8d6e26 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md +++ b/application/src/main/data/json/edge/instructions/upgrade/ubuntu/instructions.md @@ -1,15 +1,14 @@ #### Upgrading to ${TB_EDGE_VERSION}EDGE -**ThingsBoard Edge package download:** +ThingsBoard Edge package download: ```bash wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.deb {:copy-code} ``` -##### ThingsBoard Edge service upgrade - Install package: ```bash sudo dpkg -i tb-edge-${TB_EDGE_TAG}.deb {:copy-code} ``` + ${UPGRADE_DB} diff --git a/application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md b/application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md index bdd6c4ea18..3d3ac8c705 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md +++ b/application/src/main/data/json/edge/instructions/upgrade/upgrade_preparing.md @@ -1,6 +1,6 @@ -Here is the list of commands, that can be used to quickly upgrade ThingsBoard Edge on ${OS} +Here is the list of commands that can be used to quickly upgrade ThingsBoard Edge on ${OS} -#### Prepare for upgrading ThingsBoard Edge +#### Prepare for Upgrading ThingsBoard Edge Stop ThingsBoard Edge service: @@ -33,4 +33,4 @@ sudo -Hiu postgres pg_dump tb_edge > tb_edge.sql.bak {:copy-code} ``` -Check backup file created successfully. +Check the backup file created successfully. diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index bb8c3fca44..654ae8bf29 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -670,7 +670,7 @@ public class ActorSystemContext { @Getter private long cfCheckInterval; - @Value("${actors.alarms.reevaluation_interval:120}") + @Value("${actors.alarms.reevaluation_interval:60}") @Getter private long alarmRulesReevaluationInterval; diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index da16e55db8..cfac432017 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -178,7 +178,7 @@ public class AppActor extends ContextAwareActor { }); } } - if (!msg.getEntityId().getEntityType().isOneOf(EntityType.TENANT_PROFILE, EntityType.TB_RESOURCE)) { + if (!msg.getEntityId().getEntityType().isOneOf(EntityType.TENANT_PROFILE, EntityType.TB_RESOURCE, EntityType.USER)) { log.warn("Message has system tenant id: {}", msg); } } else { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 4022526aab..b19ad1a8b4 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -180,7 +180,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware cfsReevaluationTask = systemContext.getScheduler().scheduleWithFixedDelay(() -> { try { calculatedFields.values().forEach(cf -> { - if (cf.isRequiresScheduledReevaluation()) { + if (cf.requiresScheduledReevaluation()) { applyToTargetCfEntityActors(cf, TbCallback.EMPTY, (entityId, callback) -> { log.debug("[{}][{}] Pushing scheduled CF reevaluate msg", entityId, cf.getCfId()); getOrCreateActor(entityId).tell(new CalculatedFieldReevaluateMsg(tenantId, cf)); diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index 410b05456e..f6851a5a71 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -86,6 +86,9 @@ public class SwaggerConfiguration { public static final String LOGIN_ENDPOINT = "/api/auth/login"; public static final String REFRESH_TOKEN_ENDPOINT = "/api/auth/token"; + private static final String LOGIN_PASSWORD_SCHEME = "HTTP login form"; + private static final String API_KEY_SCHEME = "API key form"; + private static final ApiResponses loginResponses = loginResponses(); private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false); private static final ApiResponses defaultPostErrorResponses = defaultErrorResponses(true); @@ -142,14 +145,28 @@ public class SwaggerConfiguration { .license(license) .version(apiVersion); - SecurityScheme securityScheme = new SecurityScheme() + SecurityScheme loginPasswordScheme = new SecurityScheme() .type(SecurityScheme.Type.HTTP) .description("Enter Username / Password") .scheme("loginPassword") .bearerFormat("/api/auth/login|X-Authorization"); + SecurityScheme apiKeyScheme = new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .name("X-Authorization") + .in(SecurityScheme.In.HEADER) + .description(""" + Enter the API key value with 'ApiKey' prefix in format: **ApiKey ** + + Example: **ApiKey tb_5te51SkLRYpjGrujUGwqkjFvooWBlQpVe2An2Dr3w13wjfxDW** + +
**NOTE**: Use only ONE authentication method at a time. If both are authorized, JWT auth takes the priority.
+ """); + var openApi = new OpenAPI() - .components(new Components().addSecuritySchemes("HTTP login form", securityScheme)) + .components(new Components() + .addSecuritySchemes(LOGIN_PASSWORD_SCHEME, loginPasswordScheme) + .addSecuritySchemes(API_KEY_SCHEME, apiKeyScheme)) .info(info); addDefaultSchemas(openApi); addLoginOperation(openApi); @@ -198,13 +215,14 @@ public class SwaggerConfiguration { operation.summary("Login method to get user JWT token data"); operation.description(""" Login method used to authenticate user and get JWT token data. - + Value of the response **token** field can be used as **X-Authorization** header value: - + `X-Authorization: Bearer $JWT_TOKEN_VALUE`."""); + var requestBody = new RequestBody().description("Login request") .content(new Content().addMediaType(APPLICATION_JSON_VALUE, - new MediaType().schema(new Schema().$ref("#/components/schemas/LoginRequest")))); + new MediaType().schema(new Schema().$ref("#/components/schemas/LoginRequest")))); operation.requestBody(requestBody); operation.responses(loginResponses); @@ -218,11 +236,11 @@ public class SwaggerConfiguration { var operation = new Operation(); operation.summary("Refresh user JWT token data"); operation.description(""" - Method to refresh JWT token. Provide a valid refresh token to get a new JWT token. - - The response contains a new token that can be used for authorization. - - `X-Authorization: Bearer $JWT_TOKEN_VALUE`"""); + Method to refresh JWT token. Provide a valid refresh token to get a new JWT token. + + The response contains a new token that can be used for authorization. + + `X-Authorization: Bearer $JWT_TOKEN_VALUE`"""); var requestBody = new RequestBody().description("Refresh token request") .content(new Content().addMediaType(APPLICATION_JSON_VALUE, @@ -291,8 +309,9 @@ public class SwaggerConfiguration { return (routerOperation, handlerMethod) -> { String[] pNames = localSpringDocParameterNameDiscoverer.getParameterNames(handlerMethod.getMethod()); String[] reflectionParametersNames = Arrays.stream(handlerMethod.getMethod().getParameters()).map(java.lang.reflect.Parameter::getName).toArray(String[]::new); - if (pNames == null || Arrays.stream(pNames).anyMatch(Objects::isNull)) + if (pNames == null || Arrays.stream(pNames).anyMatch(Objects::isNull)) { pNames = reflectionParametersNames; + } MethodParameter[] parameters = handlerMethod.getMethodParameters(); List requestParams = new ArrayList<>(); for (var i = 0; i < parameters.length; i++) { @@ -324,26 +343,25 @@ public class SwaggerConfiguration { } private OpenApiCustomizer customOpenApiCustomizer() { - var loginForm = new SecurityRequirement().addList("HTTP login form", Arrays.asList( - Authority.SYS_ADMIN.name(), - Authority.TENANT_ADMIN.name(), - Authority.CUSTOMER_USER.name() - )); + var loginRequirement = createSecurityRequirement(LOGIN_PASSWORD_SCHEME); + var apiKeyRequirement = createSecurityRequirement(API_KEY_SCHEME); + return openAPI -> { var paths = openAPI.getPaths(); - paths.entrySet().stream().peek(entry -> { - securityCustomization(loginForm, entry); - if (!entry.getKey().equals(LOGIN_ENDPOINT)) { - defaultErrorResponsesCustomization(entry.getValue()); - } - }).map(this::tagsCustomization).filter(Objects::nonNull).distinct().sorted(Comparator.comparing(Tag::getName)).forEach(openAPI::addTagsItem); + paths.entrySet().stream() + .peek(entry -> { + securityCustomization(entry, loginRequirement, apiKeyRequirement); + if (!entry.getKey().equals(LOGIN_ENDPOINT)) { + defaultErrorResponsesCustomization(entry.getValue()); + } + }) + .map(this::extractTagFromPath).filter(Objects::nonNull).distinct().sorted(Comparator.comparing(Tag::getName)).forEach(openAPI::addTagsItem); var pathItemsByTags = new TreeMap>(); paths.forEach((k, v) -> { var tagItem = tagItemFromPathItem(v); if (tagItem != null) { - var pathItemMap = pathItemsByTags.computeIfAbsent(tagItem, k1 -> new TreeMap<>()); - pathItemMap.put(k, v); + pathItemsByTags.computeIfAbsent(tagItem, k1 -> new TreeMap<>()).put(k, v); } }); var sortedPaths = new Paths(); @@ -357,13 +375,17 @@ public class SwaggerConfiguration { }; } + private SecurityRequirement createSecurityRequirement(String schemeName) { + return new SecurityRequirement().addList(schemeName, Arrays.asList( + Authority.SYS_ADMIN.name(), + Authority.TENANT_ADMIN.name(), + Authority.CUSTOMER_USER.name() + )); + } - private Tag tagsCustomization(Map.Entry entry) { - var tagItem = tagItemFromPathItem(entry.getValue()); - if (tagItem != null) { - return tagFromTagItem(tagItem); - } - return null; + private Tag extractTagFromPath(Map.Entry entry) { + var tagName = tagItemFromPathItem(entry.getValue()); + return tagName != null ? tagFromTagItem(tagName) : null; } private String tagItemFromPathItem(PathItem item) { @@ -383,17 +405,20 @@ public class SwaggerConfiguration { StringBuilder sb = new StringBuilder(); for (String word : words) { - sb.append(word.substring(0, 1).toUpperCase()); - sb.append(word.substring(1).toLowerCase()); - sb.append(" "); + if (!word.isEmpty()) { + sb.append(word.substring(0, 1).toUpperCase()); + sb.append(word.substring(1).toLowerCase()); + sb.append(" "); + } } return new Tag().name(tagItem).description(sb.toString().trim()); } private void defaultErrorResponsesCustomization(PathItem pathItem) { - pathItem.readOperationsMap().forEach(((httpMethod, operation) -> { + pathItem.readOperationsMap().forEach((httpMethod, operation) -> { var errorResponses = httpMethod.equals(PathItem.HttpMethod.POST) ? defaultPostErrorResponses : defaultErrorResponses; + var responses = operation.getResponses(); if (responses == null) { responses = errorResponses; @@ -406,16 +431,19 @@ public class SwaggerConfiguration { }); } operation.setResponses(responses); - })); + }); } - private void securityCustomization(SecurityRequirement loginForm, Map.Entry entry) { + private void securityCustomization(Map.Entry entry, SecurityRequirement jwtBearerRequirement, SecurityRequirement apiKeyRequirement) { var path = entry.getKey(); - if (path.matches(securityPathRegex) && !path.matches(nonSecurityPathRegex) && !path.equals(LOGIN_ENDPOINT)) { + if (path.matches(securityPathRegex) && !path.matches(nonSecurityPathRegex) && !path.equals(LOGIN_ENDPOINT) && !path.equals(REFRESH_TOKEN_ENDPOINT)) { entry.getValue() .readOperationsMap() .values() - .forEach(operation -> operation.addSecurityItem(loginForm)); + .forEach(operation -> { + operation.addSecurityItem(jwtBearerRequirement); + operation.addSecurityItem(apiKeyRequirement); + }); } } @@ -430,6 +458,7 @@ public class SwaggerConfiguration { private static ApiResponses defaultErrorResponses(boolean isPost) { ApiResponses apiResponses = new ApiResponses(); + apiResponses.addApiResponse("400", errorResponse("400", "Bad Request", ThingsboardErrorResponse.of(isPost ? "Invalid request body" : "Invalid UUID string: 123", ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST))); @@ -465,8 +494,7 @@ public class SwaggerConfiguration { ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)) ) )); - var credentialsExpiredSchema = new Schema(); - credentialsExpiredSchema.$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse"); + var credentialsExpiredSchema = new Schema().$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse"); apiResponses.addApiResponse("401 ", errorResponse("Unauthorized (**Expired credentials**)", Map.of( "credentials-expired", errorExample("Expired credentials", @@ -482,15 +510,13 @@ public class SwaggerConfiguration { } private static ApiResponse errorResponse(String description, Map examples) { - var schema = new Schema(); - schema.$ref("#/components/schemas/ThingsboardErrorResponse"); + var schema = new Schema().$ref("#/components/schemas/ThingsboardErrorResponse"); return errorResponse(description, examples, schema); } private static ApiResponse errorResponse(String description, Map examples, Schema errorResponseSchema) { - MediaType mediaType = new MediaType().schema(errorResponseSchema); - mediaType.setExamples(examples); - Content content = new Content().addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, mediaType); + MediaType mediaType = new MediaType().schema(errorResponseSchema).examples(examples); + Content content = new Content().addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, mediaType); return new ApiResponse().description(description).content(content); } diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java index 70fa3a5ed0..f471c9e7b4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java @@ -17,33 +17,30 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; +import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.edqs.EdqsState; import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.AvailableEntityKeys; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityFilter; -import org.thingsboard.server.common.msg.edqs.EdqsApiService; import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -51,52 +48,46 @@ import org.thingsboard.server.service.query.EntityQueryService; import org.thingsboard.server.service.security.permission.Operation; import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_SCOPE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_DATA_QUERY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; @RestController @TbCoreComponent @RequestMapping("/api") +@RequiredArgsConstructor public class EntityQueryController extends BaseController { - @Autowired - private EntityQueryService entityQueryService; - @Autowired - private EdqsService edqsService; - @Autowired - private EdqsApiService edqsApiService; + private final EntityQueryService entityQueryService; + private final EdqsService edqsService; private static final int MAX_PAGE_SIZE = 100; @ApiOperation(value = "Count Entities by Query", notes = ENTITY_COUNT_QUERY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/entitiesQuery/count", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/entitiesQuery/count") public long countEntitiesByQuery( @Parameter(description = "A JSON value representing the entity count query. See API call notes above for more details.") @RequestBody EntityCountQuery query) throws ThingsboardException { checkNotNull(query); resolveQuery(query); - return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query); + return entityQueryService.countEntitiesByQuery(getCurrentUser(), query); } @ApiOperation(value = "Find Entity Data by Query", notes = ENTITY_DATA_QUERY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/entitiesQuery/find", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/entitiesQuery/find") public PageData findEntityDataByQuery( @Parameter(description = "A JSON value representing the entity data query. See API call notes above for more details.") @RequestBody EntityDataQuery query) throws ThingsboardException { checkNotNull(query); resolveQuery(query); - return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query); + return entityQueryService.findEntityDataByQuery(getCurrentUser(), query); } @ApiOperation(value = "Find Alarms by Query", notes = ALARM_DATA_QUERY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarmsQuery/find", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/alarmsQuery/find") public PageData findAlarmDataByQuery( @Parameter(description = "A JSON value representing the alarm data query. See API call notes above for more details.") @RequestBody AlarmDataQuery query) throws ThingsboardException { @@ -107,13 +98,12 @@ public class EntityQueryController extends BaseController { checkUserId(assigneeId, Operation.READ); } resolveQuery(query); - return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); + return entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); } @ApiOperation(value = "Count Alarms by Query (countAlarmsByQuery)", notes = "Returns the number of alarms that match the query definition.") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarmsQuery/count", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/alarmsQuery/count") public long countAlarmsByQuery(@Parameter(description = "A JSON value representing the alarm count query.") @RequestBody AlarmCountQuery query) throws ThingsboardException { checkNotNull(query); @@ -122,31 +112,47 @@ public class EntityQueryController extends BaseController { checkUserId(assigneeId, Operation.READ); } resolveQuery(query); - return this.entityQueryService.countAlarmsByQuery(getCurrentUser(), query); + return entityQueryService.countAlarmsByQuery(getCurrentUser(), query); } - @ApiOperation(value = "Find Entity Keys by Query", - notes = "Uses entity data query (see 'Find Entity Data by Query') to find first 100 entities. Then fetch and return all unique time-series and/or attribute keys. Used mostly for UI hints.") + @ApiOperation( + value = "Find Available Entity Keys by Query", + notes = """ + Returns unique time series and/or attribute key names from entities matching the query.\n + Executes the Entity Data Query to find up to 100 entities, then fetches and aggregates all distinct key names.\n + Primarily used for UI features like autocomplete suggestions.""" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH + ) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/entitiesQuery/find/keys", method = RequestMethod.POST) - @ResponseBody - public DeferredResult findEntityTimeseriesAndAttributesKeysByQuery( - @Parameter(description = "A JSON value representing the entity data query. See API call notes above for more details.") + @PostMapping("/entitiesQuery/find/keys") + public DeferredResult findAvailableEntityKeysByQuery( + @Parameter(description = "Entity data query to find entities. Page size is capped at 100.") @RequestBody EntityDataQuery query, - @Parameter(description = "Include all unique time-series keys to the result.") - @RequestParam("timeseries") boolean isTimeseries, - @Parameter(description = "Include all unique attribute keys to the result.") - @RequestParam("attributes") boolean isAttributes, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) - @RequestParam(value = "scope", required = false) String scope) throws ThingsboardException { - TenantId tenantId = getTenantId(); - checkNotNull(query); + + // fixme: combination of timeseries = false and attributes = false is allowed, but always results in empty response, therefore does not make any sense + // such combinations should NOT be allowed, but changing this will break clients + + @Parameter(description = """ + When true, includes unique time series key names in the response. + When false, the 'timeseries' list will be empty.""") + @RequestParam("timeseries") boolean includeTimeseries, + + @Parameter(description = """ + When true, includes unique attribute key names in the response. + When false, the 'attribute' list will be empty. Use 'scope' parameter to filter by attribute scope.""") + @RequestParam("attributes") boolean includeAttributes, + + @Parameter(description = """ + Filters attribute keys by scope. Only applies when 'attributes' is true. + If not specified, returns attribute keys from all scopes.""", + schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) + @RequestParam(value = "scope", required = false) AttributeScope scope + ) throws ThingsboardException { resolveQuery(query); EntityDataPageLink pageLink = query.getPageLink(); if (pageLink.getPageSize() > MAX_PAGE_SIZE) { pageLink.setPageSize(MAX_PAGE_SIZE); } - return entityQueryService.getKeysByQuery(getCurrentUser(), tenantId, query, isTimeseries, isAttributes, scope); + return wrapFuture(entityQueryService.getKeysByQuery(getCurrentUser(), getTenantId(), query, includeTimeseries, includeAttributes, scope)); } @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index 54d2494679..394b745032 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -32,12 +32,16 @@ import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; @@ -215,6 +219,7 @@ public class TbResourceController extends BaseController { "\n\nResource combination of the title with the key is unique in the scope of tenant. " + "Remove 'id', 'tenantId' from the request body example (below) to create new Resource entity." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Deprecated // resource should be save or update with an upload request @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PostMapping(value = "/resource") public TbResourceInfo saveResource(@Parameter(description = "A JSON value representing the Resource.") @@ -224,6 +229,71 @@ public class TbResourceController extends BaseController { return tbResourceService.save(resource, getCurrentUser()); } + @ApiOperation(value = "Upload Resource via Multipart File (uploadResource)", + notes = "Create the Resource using multipart file upload. " + + "\n\nResource combination of the title with the key is unique in the scope of tenant. " + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PostMapping(value = "/resource/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public TbResourceInfo uploadResource(@Parameter(description = "Resource title.", example = "Title") + @RequestPart(name = "title", required = false) String title, + @Parameter(description = "Resource type.") + @RequestPart(name = "resourceType") String resourceTypeStr, + @Parameter(description = "Resource descriptor (JSON).") + @RequestPart(name = "descriptor", required = false) String descriptor, + @Parameter(description = "Resource sub type.") + @RequestPart(name = "resourceSubType", required = false) String resourceSubTypeStr, + @Parameter(description = "Resource file.") + @RequestPart MultipartFile file) throws Exception { + TbResource resource = new TbResource(); + resource.setTenantId(getTenantId()); + resource.setTitle(StringUtils.isNotEmpty(title) ? title : file.getOriginalFilename()); + ResourceType resourceType = ResourceType.valueOf(resourceTypeStr); + resource.setResourceType(resourceType); + + if (StringUtils.isNotEmpty(descriptor)) { + resource.setDescriptor(JacksonUtil.toJsonNode(descriptor)); + } else { + String mediaType = resourceType.getMediaType() != null ? resourceType.getMediaType() : file.getContentType(); + resource.setDescriptor(JacksonUtil.newObjectNode().put("mediaType", mediaType)); + } + + if (StringUtils.isNotEmpty(resourceSubTypeStr)) { + resource.setResourceSubType(ResourceSubType.valueOf(resourceSubTypeStr)); + } + resource.setFileName(file.getOriginalFilename()); + resource.setData(file.getBytes()); + + checkEntity(resource.getId(), resource, Resource.TB_RESOURCE); + return tbResourceService.save(resource, getCurrentUser()); + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PutMapping(value = "/resource/{id}/data", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public TbResourceInfo updateResourceData(@Parameter(description = "Unique identifier of the Resource to update", required = true) + @PathVariable UUID id, + @Parameter(description = "Resource file.") + @RequestPart MultipartFile file) throws Exception { + TbResourceId tbResourceId = new TbResourceId(id); + TbResource resource = checkResourceId(tbResourceId, Operation.WRITE); + resource.setFileName(file.getOriginalFilename()); + resource.setData(file.getBytes()); + return tbResourceService.save(resource, getCurrentUser()); + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PutMapping("/resource/{id}/info") + public TbResourceInfo updateResourceInfo(@Parameter(description = "Unique identifier of the Resource to update", required = true) + @PathVariable UUID id, + @Parameter(description = "A JSON value representing the Resource Info.") + @RequestBody TbResourceInfo resourceInfo) throws Exception { + TbResourceId tbResourceId = new TbResourceId(id); + checkResourceInfoId(tbResourceId, Operation.WRITE); + resourceInfo.setId(tbResourceId); + TbResource resource = new TbResource(resourceInfo); + return tbResourceService.save(resource, getCurrentUser()); + } + @ApiOperation(value = "Get Resource Infos (getResources)", notes = "Returns a page of Resource Info objects owned by tenant or sysadmin. " + PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index 83a99714ad..9df5b924fb 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -112,7 +112,7 @@ public class TenantController extends BaseController { } @ApiOperation(value = "Delete Tenant (deleteTenant)", - notes = "Deletes the tenant, it's customers, rule chains, devices and all other related entities. Referencing non-existing tenant Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) + notes = "Deletes the tenant, it's customers, rule chains, devices and all other related entities. Referencing non-existing tenant Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @DeleteMapping(value = "/tenant/{tenantId}") @ResponseStatus(value = HttpStatus.OK) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index 845d5a50e9..13ceb406bb 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -54,9 +54,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -75,7 +73,6 @@ import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; 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.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; @@ -84,7 +81,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.function.Function; @@ -101,7 +97,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATE import static org.thingsboard.server.dao.util.KvUtils.filterChangedAttr; import static org.thingsboard.server.dao.util.KvUtils.toTsKvEntryList; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultAttributeEntry; -import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultKvEntry; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultTsKvEntry; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformAggMetricArgument; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformAggregationArgument; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformSingleValueArgument; @@ -210,14 +206,14 @@ public abstract class AbstractCalculatedFieldProcessingService { default -> { var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry); argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds -> - fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue()), MoreExecutors.directExecutor())); + fetchGeofencingArgumentValue(ctx.getTenantId(), resolvedEntityIds, entry.getValue(), startTs), MoreExecutors.directExecutor())); } } } return argFutures; } - protected Map> fetchRelatedEntitiesAggArguments(CalculatedFieldCtx ctx, EntityId entityId, long ts) { + private Map> fetchRelatedEntitiesAggArguments(CalculatedFieldCtx ctx, EntityId entityId, long ts) { if (!(ctx.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config)) { return Collections.emptyMap(); } @@ -230,7 +226,7 @@ public abstract class AbstractCalculatedFieldProcessingService { )); } - protected Map> fetchEntityAggArguments(CalculatedFieldCtx ctx, EntityId entityId, long ts) { + private Map> fetchEntityAggArguments(CalculatedFieldCtx ctx, EntityId entityId, long ts) { if (!(ctx.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration config)) { return Collections.emptyMap(); } @@ -291,31 +287,28 @@ public abstract class AbstractCalculatedFieldProcessingService { return ownerService.getOwner(tenantId, entityId); } - private ListenableFuture fetchGeofencingKvEntry(TenantId tenantId, List geofencingEntities, Argument argument) { + private ListenableFuture fetchGeofencingArgumentValue(TenantId tenantId, List geofencingEntities, Argument argument, long startTs) { if (argument.getRefEntityKey().getType() != ArgumentType.ATTRIBUTE) { throw new IllegalStateException("Unsupported argument key type: " + argument.getRefEntityKey().getType()); } - List>> kvFutures = geofencingEntities.stream() + var geofencingEntityIdToKvEntryMapFutures = Futures.allAsList(fetchGeofencingEntityIdToKvEntriesFutures(tenantId, geofencingEntities, argument, startTs)); + return Futures.transform(geofencingEntityIdToKvEntryMapFutures, entries -> ArgumentEntry.createGeofencingValueArgument(entries.stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))), MoreExecutors.directExecutor()); + } + + private List>> fetchGeofencingEntityIdToKvEntriesFutures(TenantId tenantId, List geofencingEntities, Argument argument, long startTs) { + return geofencingEntities.stream() .map(entityId -> { - var attributesFuture = attributesService.find( - tenantId, - entityId, - argument.getRefEntityKey().getScope(), - argument.getRefEntityKey().getKey() - ); + AttributeScope scope = argument.getRefEntityKey().getScope(); + String key = argument.getRefEntityKey().getKey(); + var attributesFuture = attributesService.find(tenantId, entityId, scope, key); return Futures.transform(attributesFuture, resultOpt -> - Map.entry(entityId, resultOpt.orElseGet(() -> createDefaultAttributeEntry(argument, System.currentTimeMillis()))), - calculatedFieldCallbackExecutor - ); + Map.entry(entityId, resultOpt.orElseGet(() -> createDefaultAttributeEntry(argument, startTs))), + calculatedFieldCallbackExecutor); }).collect(Collectors.toList()); - - ListenableFuture>> allFutures = Futures.allAsList(kvFutures); - - return Futures.transform(allFutures, entries -> ArgumentEntry.createGeofencingValueArgument(entries.stream() - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))), MoreExecutors.directExecutor()); } - public ListenableFuture fetchRelatedEntitiesArgumentEntry(TenantId tenantId, List aggEntities, Argument argument, long startTs) { + private ListenableFuture fetchRelatedEntitiesArgumentEntry(TenantId tenantId, List aggEntities, Argument argument, long startTs) { List>> futures = aggEntities.stream() .map(entityId -> { ListenableFuture argumentEntryFut = fetchArgumentValue(tenantId, entityId, argument, startTs); @@ -368,21 +361,17 @@ public abstract class AbstractCalculatedFieldProcessingService { return Futures.transform(attributeOptFuture, attrOpt -> { log.debug("[{}][{}] Fetched attribute for key {}: {}", tenantId, entityId, argument.getRefEntityKey(), attrOpt); - AttributeKvEntry attributeKvEntry = attrOpt.orElseGet(() -> new BaseAttributeKvEntry(createDefaultKvEntry(argument), defaultLastUpdateTs, SingleValueArgumentEntry.DEFAULT_VERSION)); - return transformSingleValueArgument(Optional.of(attributeKvEntry)); + return transformSingleValueArgument(attrOpt.orElseGet(() -> createDefaultAttributeEntry(argument, defaultLastUpdateTs))); }, calculatedFieldCallbackExecutor); } - protected ListenableFuture fetchTsLatest(TenantId tenantId, EntityId entityId, Argument argument, long defaultTs) { + private ListenableFuture fetchTsLatest(TenantId tenantId, EntityId entityId, Argument argument, long defaultTs) { String timeseriesKey = argument.getRefEntityKey().getKey(); log.trace("[{}][{}] Fetching latest timeseries {}", tenantId, entityId, timeseriesKey); - return transformSingleValueArgument( - Futures.transform( - timeseriesService.findLatest(tenantId, entityId, timeseriesKey), - result -> { - log.debug("[{}][{}] Fetched latest timeseries {}: {}", tenantId, entityId, timeseriesKey, result); - return result.or(() -> Optional.of(new BasicTsKvEntry(defaultTs, createDefaultKvEntry(argument), SingleValueArgumentEntry.DEFAULT_VERSION))); - }, calculatedFieldCallbackExecutor)); + return Futures.transform(timeseriesService.findLatest(tenantId, entityId, timeseriesKey), result -> { + log.debug("[{}][{}] Fetched latest timeseries {}: {}", tenantId, entityId, timeseriesKey, result); + return transformSingleValueArgument(result.orElseGet(() -> createDefaultTsKvEntry(argument, defaultTs))); + }, calculatedFieldCallbackExecutor); } private ListenableFuture fetchTimeSeriesInternal(TenantId tenantId, EntityId entityId, ReadTsKvQuery query, Function, ArgumentEntry> transformArgument) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java index f2b8d4d9db..e7c4801c12 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageDataIterable; @@ -243,11 +244,12 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { @Override public EntityId getProfileId(TenantId tenantId, EntityId entityId) { - return switch (entityId.getEntityType()) { - case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId(); - case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId(); + HasId profile = switch (entityId.getEntityType()) { + case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId); + case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId); default -> null; }; + return profile != null ? profile.getId() : null; } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index bbb072049d..95a524187a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -41,6 +41,8 @@ import java.util.stream.Collectors; @Getter public abstract class BaseCalculatedFieldState implements CalculatedFieldState, Closeable { + protected static final long DEFAULT_LAST_UPDATE_TS = -1L; + protected final EntityId entityId; protected CalculatedFieldCtx ctx; protected TbActorRef actorCtx; @@ -48,7 +50,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, protected Map arguments = new HashMap<>(); protected boolean sizeExceedsLimit; - protected long latestTimestamp = -1; + protected long latestTimestamp = DEFAULT_LAST_UPDATE_TS; protected ReadinessStatus readinessStatus; @Setter @@ -119,7 +121,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, requiredArguments = null; arguments.clear(); sizeExceedsLimit = false; - latestTimestamp = -1; + latestTimestamp = DEFAULT_LAST_UPDATE_TS; } @Override @@ -147,7 +149,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, return valuesNode; } long latestTs = getLatestTimestamp(); - if (latestTs == -1) { + if (latestTs == DEFAULT_LAST_UPDATE_TS) { return valuesNode; } ObjectNode resultNode = JacksonUtil.newObjectNode(); @@ -162,15 +164,15 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, newTs = singleValueArgumentEntry.getTs(); } else if (entry instanceof TsRollingArgumentEntry tsRollingArgumentEntry) { Map.Entry lastEntry = tsRollingArgumentEntry.getTsRecords().lastEntry(); - newTs = (lastEntry != null) ? lastEntry.getKey() : System.currentTimeMillis(); + newTs = (lastEntry != null) ? lastEntry.getKey() : DEFAULT_LAST_UPDATE_TS; } else if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { newTs = relatedEntitiesArgumentEntry.getEntityInputs().values().stream() - .mapToLong(e -> (e instanceof SingleValueArgumentEntry s) ? s.getTs() : 0L) + .mapToLong(e -> (e instanceof SingleValueArgumentEntry s) ? s.getTs() : DEFAULT_LAST_UPDATE_TS) .max() - .orElse(0L); + .orElse(DEFAULT_LAST_UPDATE_TS); } else if (entry instanceof GeofencingArgumentEntry geofencingArgumentEntry) { newTs = geofencingArgumentEntry.getZoneStates().values().stream() - .mapToLong(GeofencingZoneState::getTs).max().orElse(0L); + .mapToLong(GeofencingZoneState::getTs).max().orElse(DEFAULT_LAST_UPDATE_TS); } this.latestTimestamp = Math.max(this.latestTimestamp, newTs); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index a6234ad3cc..c2c65d3327 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -79,10 +79,14 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS; + @Data @Slf4j public class CalculatedFieldCtx implements Closeable { + public static final long DISABLED_INTERVAL_VALUE = -1L; + private CalculatedField calculatedField; private CalculatedFieldId cfId; @@ -197,7 +201,7 @@ public class CalculatedFieldCtx implements Closeable { } } if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledConfig) { - this.scheduledUpdateIntervalMillis = scheduledConfig.isScheduledUpdateEnabled() ? TimeUnit.SECONDS.toMillis(scheduledConfig.getScheduledUpdateInterval()) : -1L; + this.scheduledUpdateIntervalMillis = scheduledConfig.isScheduledUpdateEnabled() ? TimeUnit.SECONDS.toMillis(scheduledConfig.getScheduledUpdateInterval()) : DISABLED_INTERVAL_VALUE; } if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration aggConfig) { this.useLatestTs = aggConfig.isUseLatestTs(); @@ -212,7 +216,7 @@ public class CalculatedFieldCtx implements Closeable { this.maxSingleValueArgumentSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; } - public boolean isRequiresScheduledReevaluation() { + public boolean requiresScheduledReevaluation() { long now = System.currentTimeMillis(); if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration entityAggregationConfig) { Watermark watermark = entityAggregationConfig.getWatermark(); @@ -232,8 +236,8 @@ public class CalculatedFieldCtx implements Closeable { } boolean requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation(); if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration) { - long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval()); if (requiresScheduledReevaluation) { + long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval()); if (now - lastReevaluationTs >= reevaluationIntervalMillis) { lastReevaluationTs = now; return true; @@ -642,6 +646,9 @@ public class CalculatedFieldCtx implements Closeable { // if the rules have any changes not tracked by hasStateChanges return true; } + if (!thisConfig.propagationSettingsEqual(otherConfig)) { + return true; + } } if (scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis) { return true; @@ -712,8 +719,8 @@ public class CalculatedFieldCtx implements Closeable { return false; } - private boolean isScheduledUpdateEnabled() { - return scheduledUpdateIntervalMillis != -1; + private boolean isScheduledUpdateDisabled() { + return scheduledUpdateIntervalMillis == DISABLED_INTERVAL_VALUE; } public boolean shouldFetchRelationQueryDynamicArgumentsFromDb(CalculatedFieldState state) { @@ -723,11 +730,11 @@ public class CalculatedFieldCtx implements Closeable { return switch (cfType) { case PROPAGATION -> true; case GEOFENCING -> { - if (!isScheduledUpdateEnabled()) { + if (isScheduledUpdateDisabled()) { yield false; } var geofencingState = (GeofencingCalculatedFieldState) state; - if (geofencingState.getLastDynamicArgumentsRefreshTs() == -1L) { + if (geofencingState.getLastDynamicArgumentsRefreshTs() == DEFAULT_LAST_UPDATE_TS) { yield true; } yield geofencingState.getLastDynamicArgumentsRefreshTs() < @@ -741,10 +748,10 @@ public class CalculatedFieldCtx implements Closeable { if (!(state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState)) { return false; } - if (!isScheduledUpdateEnabled()) { + if (isScheduledUpdateDisabled()) { return false; } - if (relatedEntitiesAggState.getLastRelatedEntitiesRefreshTs() == -1L) { + if (relatedEntitiesAggState.getLastRelatedEntitiesRefreshTs() == DEFAULT_LAST_UPDATE_TS) { return true; } return relatedEntitiesAggState.getLastRelatedEntitiesRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java index e8174bfd57..ffc3c1584b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java @@ -43,7 +43,6 @@ import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; import org.thingsboard.server.service.cf.AbstractCalculatedFieldStateService; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToString; @@ -105,11 +104,6 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta this.stateProducer = (TbKafkaProducerTemplate>) queueFactory.createCalculatedFieldStateProducer(); } - @Override - public void restore(QueueKey queueKey, Set partitions) { - stateService.update(queueKey, partitions, null); - } - @Override protected void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback) { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_STATES_QUEUE_NAME, stateId.tenantId(), stateId.entityId()); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java index d8e4fcf5d4..97916192b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.type.TypeReference; import lombok.AllArgsConstructor; import lombok.Data; -import lombok.NoArgsConstructor; import org.springframework.lang.Nullable; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbelCfArg; @@ -34,11 +33,14 @@ import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; +import static org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState.DEFAULT_LAST_UPDATE_TS; + @Data -@NoArgsConstructor @AllArgsConstructor public class SingleValueArgumentEntry implements ArgumentEntry { + public static final Long DEFAULT_VERSION = -1L; + @Nullable protected EntityId entityId; @@ -48,7 +50,10 @@ public class SingleValueArgumentEntry implements ArgumentEntry { protected boolean forceResetPrevious; - public static final Long DEFAULT_VERSION = -1L; + public SingleValueArgumentEntry() { + this.ts = DEFAULT_LAST_UPDATE_TS; + this.version = DEFAULT_VERSION; + } public SingleValueArgumentEntry(EntityId entityId, ArgumentEntry entry) { this(entry); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java index 264f651eb0..7036e4bd77 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java @@ -51,18 +51,19 @@ import java.util.concurrent.ScheduledFuture; import java.util.stream.Collectors; import static java.util.concurrent.TimeUnit.SECONDS; +import static org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx.DISABLED_INTERVAL_VALUE; @Slf4j @Getter public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculatedFieldState { @Setter - private long lastArgsRefreshTs = -1; + private long lastArgsRefreshTs = DEFAULT_LAST_UPDATE_TS; @Setter - private long lastMetricsEvalTs = -1; + private long lastMetricsEvalTs = DEFAULT_LAST_UPDATE_TS; @Setter - private long lastRelatedEntitiesRefreshTs = -1; - private long deduplicationIntervalMs = -1; + private long lastRelatedEntitiesRefreshTs = DEFAULT_LAST_UPDATE_TS; + private long deduplicationIntervalMs = DISABLED_INTERVAL_VALUE; private Map metrics; private ScheduledFuture reevaluationFuture; @@ -102,9 +103,9 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat @Override public void reset() { // must reset everything dependent on arguments super.reset(); - lastArgsRefreshTs = -1; - lastMetricsEvalTs = -1; - lastRelatedEntitiesRefreshTs = -1; + lastArgsRefreshTs = DEFAULT_LAST_UPDATE_TS; + lastMetricsEvalTs = DEFAULT_LAST_UPDATE_TS; + lastRelatedEntitiesRefreshTs = DEFAULT_LAST_UPDATE_TS; metrics = null; } @@ -153,7 +154,7 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat } public Map updateEntityData(Map fetchedArgs) { - lastMetricsEvalTs = -1; + lastMetricsEvalTs = DEFAULT_LAST_UPDATE_TS; return update(fetchedArgs, ctx); } @@ -162,7 +163,7 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat RelatedEntitiesArgumentEntry aggEntry = (RelatedEntitiesArgumentEntry) argEntry; aggEntry.getEntityInputs().remove(relatedEntityId); }); - lastMetricsEvalTs = -1; + lastMetricsEvalTs = DEFAULT_LAST_UPDATE_TS; lastArgsRefreshTs = System.currentTimeMillis(); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java index f3c3e8a1cc..ba0b151eb2 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -218,7 +218,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt if (argEntryIntervalStatus.getLastArgsRefreshTs() > argEntryIntervalStatus.getLastMetricsEvalTs()) { argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); processArgument(intervalEntry, argName, false, results); - } else if (argEntryIntervalStatus.getLastMetricsEvalTs() == -1) { + } else if (argEntryIntervalStatus.getLastMetricsEvalTs() == DEFAULT_LAST_UPDATE_TS) { argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); processArgument(intervalEntry, argName, true, results); } @@ -232,9 +232,9 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt if (argEntryIntervalStatus.intervalPassed(checkInterval)) { if (argEntryIntervalStatus.argsUpdated()) { argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); - argEntryIntervalStatus.setLastArgsRefreshTs(-1); + argEntryIntervalStatus.setLastArgsRefreshTs(DEFAULT_LAST_UPDATE_TS); processArgument(intervalEntry, argName, false, results); - } else if (argEntryIntervalStatus.getLastMetricsEvalTs() == -1) { + } else if (argEntryIntervalStatus.getLastMetricsEvalTs() == DEFAULT_LAST_UPDATE_TS) { argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); processArgument(intervalEntry, argName, true, results); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java index 342f7534c2..bd2c272bca 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java @@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAl import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.BooleanFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.ComplexFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NoDataFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate; import org.thingsboard.server.common.data.audit.ActionType; @@ -70,6 +71,8 @@ import java.util.function.Function; import static org.thingsboard.server.common.data.StringUtils.equalsAny; import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Cause.NEW_EVENT; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Cause.SCHEDULED_REEVALUATION; import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.FALSE; import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.NOT_YET_TRUE; import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.TRUE; @@ -212,9 +215,9 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { state.setDurationCheckFuture(future); } } - return evalResult; + return evalResult.withCause(NEW_EVENT); } else { - return state.reeval(System.currentTimeMillis(), ctx); + return state.reeval(System.currentTimeMillis(), ctx).withCause(SCHEDULED_REEVALUATION); } }, ctx); return Futures.immediateFuture(AlarmCalculatedFieldResult.builder() @@ -247,12 +250,12 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { private TbAlarmResult createOrClearAlarms(Function evalFunction, CalculatedFieldCtx ctx) { - TbAlarmResult result = null; + AlarmEvalResult evalResult = null; AlarmRuleState resultState = null; AlarmRuleState.StateInfo resultStateInfo = null; for (AlarmRuleState state : createRuleStates.values()) { - AlarmEvalResult evalResult = evalFunction.apply(state); + evalResult = evalFunction.apply(state); log.debug("Evaluated create rule {} with args {}. Result: {}", state, arguments, evalResult); if (evalResult.getStatus() == TRUE) { resultState = state; @@ -262,13 +265,14 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { } } + TbAlarmResult result = null; if (resultState != null) { - result = calculateAlarmResult(resultState, ctx); + result = calculateAlarmResult(resultState, evalResult, ctx); resultStateInfo = resultState.getStateInfo(); log.debug("Alarm result for state {}: {}", resultState, result); clearState(clearRuleState); } else if (currentAlarm != null && clearRuleState != null) { - AlarmEvalResult evalResult = evalFunction.apply(clearRuleState); + evalResult = evalFunction.apply(clearRuleState); log.debug("Evaluated clear rule {} with args {}. Result: {}", clearRuleState, arguments, evalResult); if (evalResult.getStatus() == TRUE) { resultStateInfo = clearRuleState.getStateInfo(); @@ -315,21 +319,25 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { } } - private TbAlarmResult calculateAlarmResult(AlarmRuleState ruleState, CalculatedFieldCtx ctx) { + private TbAlarmResult calculateAlarmResult(AlarmRuleState ruleState, AlarmEvalResult evalResult, CalculatedFieldCtx ctx) { AlarmSeverity severity = ruleState.getSeverity(); if (currentAlarm != null) { - currentAlarm.setEndTs(System.currentTimeMillis()); AlarmSeverity oldSeverity = currentAlarm.getSeverity(); - // Skip update if severity is decreased. - if (severity.ordinal() <= oldSeverity.ordinal()) { - currentAlarm.setDetails(createDetails(ruleState)); - currentAlarm.setSeverity(severity); - AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm)); - currentAlarm = result.getAlarm(); - return TbAlarmResult.fromAlarmResult(result); - } else { + if (severity.ordinal() > oldSeverity.ordinal()) { + log.trace("Skipping alarm update for result state {} for eval result {} because severity is decreased", ruleState, evalResult); return null; } + if (severity.ordinal() == oldSeverity.ordinal() && evalResult.getCause() == SCHEDULED_REEVALUATION) { + log.trace("Skipping alarm update for result state {} for eval result {}", ruleState, evalResult); + return null; + } + + currentAlarm.setEndTs(System.currentTimeMillis()); + currentAlarm.setDetails(createDetails(ruleState)); + currentAlarm.setSeverity(severity); + AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm)); + currentAlarm = result.getAlarm(); + return TbAlarmResult.fromAlarmResult(result); } else { var newAlarm = new Alarm(); newAlarm.setType(alarmType); @@ -338,7 +346,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { newAlarm.setSeverity(severity); long startTs = latestTimestamp; long currentTime = System.currentTimeMillis(); - if (startTs == 0L || startTs > currentTime) { + if (startTs <= 0L || startTs > currentTime) { startTs = currentTime; } newAlarm.setStartTs(startTs); @@ -428,6 +436,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { case STRING -> evalStrPredicate(argument, (StringFilterPredicate) predicate); case NUMERIC -> evalNumPredicate(argument, (NumericFilterPredicate) predicate); case BOOLEAN -> evalBooleanPredicate(argument, (BooleanFilterPredicate) predicate); + case NO_DATA -> evalNoDataPredicate(argument, (NoDataFilterPredicate) predicate); case COMPLEX -> evalComplexPredicate(argument, (ComplexFilterPredicate) predicate); }; } @@ -512,6 +521,18 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { }; } + private boolean evalNoDataPredicate(SingleValueArgumentEntry argument, NoDataFilterPredicate predicate) { + long passedMs = System.currentTimeMillis() - argument.getTs(); + long duration = resolveValue(predicate.getDuration(), KvUtil::getLongValue); + if (duration > 0) { + long requiredDuration = predicate.getUnit().toMillis(duration); + log.trace("[{}] No data for argument {} during {} ms, required duration: {} ms", ctx, argument, passedMs, requiredDuration); + return passedMs >= requiredDuration; + } else { + return false; + } + } + protected T resolveValue(AlarmConditionValue conditionValue, Function mapper) { T value = conditionValue.getStaticValue(); if (value == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java index 4f1a8638ee..2569f837fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java @@ -30,6 +30,7 @@ public class AlarmEvalResult { private final Status status; private final long leftDuration; private final long leftEvents; + private Cause cause; public AlarmEvalResult(Status status) { this(status, 0, 0); @@ -39,8 +40,17 @@ public class AlarmEvalResult { return new AlarmEvalResult(Status.NOT_YET_TRUE, leftDuration, leftEvents); } + public AlarmEvalResult withCause(Cause cause) { + this.cause = cause; + return this; + } + public enum Status { FALSE, NOT_YET_TRUE, TRUE; } + public enum Cause { + NEW_EVENT, SCHEDULED_REEVALUATION; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java index 4c189887b4..c6a5cbf418 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java @@ -43,6 +43,8 @@ import java.time.ZonedDateTime; import java.util.Optional; import java.util.concurrent.ScheduledFuture; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.TRUE; + @Data @Slf4j public class AlarmRuleState { @@ -81,16 +83,20 @@ public class AlarmRuleState { boolean active = isActive(ts); switch (condition.getType()) { case SIMPLE, REPEATING -> { - if (this.active == null || active != this.active) { - this.active = active; - if (active) { - return doEval(false, ctx); - } + boolean activeChanged = this.active == null || active != this.active; + this.active = active; + if (!active) { + return AlarmEvalResult.EMPTY; } - if (active) { - return AlarmEvalResult.NOT_YET_TRUE; - } else { - return AlarmEvalResult.FALSE; + + if ((condition.hasSchedule() && activeChanged) || + condition.getExpression().requiresScheduledReevaluation()) { + AlarmEvalResult result = doEval(false, ctx); + if (result.getStatus() == TRUE) { + return result; + } else { + return AlarmEvalResult.EMPTY; + } } } case DURATION -> { @@ -116,7 +122,7 @@ public class AlarmRuleState { } } } - return AlarmEvalResult.FALSE; + return AlarmEvalResult.EMPTY; } public AlarmEvalResult doEval(boolean newEvent, CalculatedFieldCtx ctx) { @@ -338,6 +344,7 @@ public class AlarmRuleState { } public record StateInfo(Long eventCount, Long duration) { + static final StateInfo EMPTY = new StateInfo(null, null); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java index a9e8eb5731..ea47dafa59 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java @@ -58,7 +58,7 @@ import static org.thingsboard.server.common.data.cf.configuration.geofencing.Geo @EqualsAndHashCode(callSuper = true) public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { - private long lastDynamicArgumentsRefreshTs = -1; + private long lastDynamicArgumentsRefreshTs = DEFAULT_LAST_UPDATE_TS; public GeofencingCalculatedFieldState(EntityId entityId) { super(entityId); @@ -147,7 +147,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { @Override public void reset() { super.reset(); - lastDynamicArgumentsRefreshTs = -1; + lastDynamicArgumentsRefreshTs = DEFAULT_LAST_UPDATE_TS; } public void updateLastDynamicArgumentsRefreshTs() { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/BaseEdgeInstallUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/BaseEdgeInstallUpgradeInstructionsService.java index e8910492a3..abce90c253 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/BaseEdgeInstallUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/BaseEdgeInstallUpgradeInstructionsService.java @@ -18,7 +18,6 @@ package org.thingsboard.server.service.edge.instructions; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.thingsboard.server.service.install.InstallScripts; import java.io.IOException; @@ -26,6 +25,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import static org.thingsboard.edge.rpc.EdgeGrpcClient.getNewestEdgeVersion; + @Slf4j @RequiredArgsConstructor public abstract class BaseEdgeInstallUpgradeInstructionsService { @@ -35,9 +36,12 @@ public abstract class BaseEdgeInstallUpgradeInstructionsService { private final InstallScripts installScripts; - @Value("${app.version:unknown}") @Setter - protected String appVersion; + protected String platformEdgeVersion = convertEdgeVersionToDocsFormat(getNewestEdgeVersion().name()); + + protected String convertEdgeVersionToDocsFormat(String edgeVersion) { + return edgeVersion.replace("_", ".").substring(2); + } protected String readFile(Path file) { try { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java index e16a8be5b5..ce37bba290 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java @@ -65,9 +65,7 @@ public class DefaultEdgeInstallInstructionsService extends BaseEdgeInstallUpgrad dockerInstallInstructions = dockerInstallInstructions.replace("${EXTRA_HOSTS}", ""); dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", baseUrl); } - String edgeVersion = appVersion + "EDGE"; - edgeVersion = edgeVersion.replace("-SNAPSHOT", ""); - dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); + dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", platformEdgeVersion + "EDGE"); dockerInstallInstructions = replacePlaceholders(dockerInstallInstructions, edge); return new EdgeInstructions(dockerInstallInstructions); } @@ -76,9 +74,8 @@ public class DefaultEdgeInstallInstructionsService extends BaseEdgeInstallUpgrad String ubuntuInstallInstructions = readFile(resolveFile(os, "instructions.md")); ubuntuInstallInstructions = replacePlaceholders(ubuntuInstallInstructions, edge); ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${BASE_URL}", request.getServerName()); - String edgeVersion = appVersion.replace("-SNAPSHOT", ""); - ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); - ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_TAG}", getTagVersion(edgeVersion)); + ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_VERSION}", platformEdgeVersion); + ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_TAG}", getTagVersion(platformEdgeVersion)); return new EdgeInstructions(ubuntuInstallInstructions); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java index 2df75d603b..8855e78683 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java @@ -52,12 +52,11 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad @Override public EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod) { - String tbVersion = appVersion.replace("-SNAPSHOT", ""); String currentEdgeVersion = convertEdgeVersionToDocsFormat(edgeVersion); return switch (upgradeMethod.toLowerCase()) { - case "docker" -> getDockerUpgradeInstructions(tbVersion, currentEdgeVersion); + case "docker" -> getDockerUpgradeInstructions(this.platformEdgeVersion, currentEdgeVersion); case "ubuntu", "centos" -> - getLinuxUpgradeInstructions(tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase()); + getLinuxUpgradeInstructions(this.platformEdgeVersion, currentEdgeVersion, upgradeMethod.toLowerCase()); default -> throw new IllegalArgumentException("Unsupported upgrade method for Edge: " + upgradeMethod); }; } @@ -74,8 +73,7 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad Optional attributeKvEntryOpt = attributesService.find(tenantId, edgeId, AttributeScope.SERVER_SCOPE, DataConstants.EDGE_VERSION_ATTR_KEY).get(); if (attributeKvEntryOpt.isPresent()) { String edgeVersionFormatted = convertEdgeVersionToDocsFormat(attributeKvEntryOpt.get().getValueAsString()); - String appVersionFormatted = appVersion.replace("-SNAPSHOT", ""); - return isVersionGreaterOrEqualsThan(edgeVersionFormatted, "3.6.0") && !isVersionGreaterOrEqualsThan(edgeVersionFormatted, appVersionFormatted); + return isVersionGreaterOrEqualsThan(edgeVersionFormatted, "3.6.0") && !isVersionGreaterOrEqualsThan(edgeVersionFormatted, platformEdgeVersion); } return false; } @@ -98,13 +96,13 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad return true; } - private EdgeInstructions getDockerUpgradeInstructions(String tbVersion, String currentEdgeVersion) { + private EdgeInstructions getDockerUpgradeInstructions(String platformEdgeVersion, String currentEdgeVersion) { EdgeUpgradeInfo edgeUpgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); - if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) { + if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || platformEdgeVersion.equals(currentEdgeVersion)) { return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available."); } StringBuilder result = new StringBuilder(readFile(resolveFile("docker", "upgrade_preparing.md"))); - while (edgeUpgradeInfo.getNextEdgeVersion() != null && !tbVersion.equals(currentEdgeVersion)) { + while (edgeUpgradeInfo.getNextEdgeVersion() != null && !platformEdgeVersion.equals(currentEdgeVersion)) { String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion(); String dockerUpgradeInstructions = readFile(resolveFile("docker", "instructions.md")); if (edgeUpgradeInfo.isRequiresUpdateDb()) { @@ -125,15 +123,15 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad return new EdgeInstructions(result.toString()); } - private EdgeInstructions getLinuxUpgradeInstructions(String tbVersion, String currentEdgeVersion, String os) { + private EdgeInstructions getLinuxUpgradeInstructions(String platformEdgeVersion, String currentEdgeVersion, String os) { EdgeUpgradeInfo edgeUpgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); - if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) { + if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || platformEdgeVersion.equals(currentEdgeVersion)) { return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available."); } String upgrade_preparing = readFile(resolveFile("upgrade_preparing.md")); upgrade_preparing = upgrade_preparing.replace("${OS}", os.equals("centos") ? "RHEL/CentOS 7/8" : "Ubuntu"); StringBuilder result = new StringBuilder(upgrade_preparing); - while (edgeUpgradeInfo.getNextEdgeVersion() != null && !tbVersion.equals(currentEdgeVersion)) { + while (edgeUpgradeInfo.getNextEdgeVersion() != null && !platformEdgeVersion.equals(currentEdgeVersion)) { String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion(); String linuxUpgradeInstructions = readFile(resolveFile(os, "instructions.md")); if (edgeUpgradeInfo.isRequiresUpdateDb()) { @@ -155,10 +153,6 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad return new EdgeInstructions(result.toString()); } - private String convertEdgeVersionToDocsFormat(String edgeVersion) { - return edgeVersion.replace("_", ".").substring(2); - } - @Override protected String getBaseDirName() { return UPGRADE_DIR; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java index d1719422ce..c2f2d51a2a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java @@ -23,6 +23,6 @@ public interface EdgeInstallInstructionsService { EdgeInstructions getInstallInstructions(Edge edge, String installationMethod, HttpServletRequest request); - void setAppVersion(String version); + void setPlatformEdgeVersion(String version); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java index 9ae68ab2d0..83a8da281d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java @@ -28,7 +28,7 @@ public interface EdgeUpgradeInstructionsService { void updateInstructionMap(Map upgradeVersions); - void setAppVersion(String version); + void setPlatformEdgeVersion(String version); boolean isUpgradeAvailable(TenantId tenantId, EdgeId edgeId) throws Exception; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index 9fcb7425b2..1827b986fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -69,6 +69,7 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -82,6 +83,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; +import java.util.function.Function; import static org.thingsboard.server.service.state.DefaultDeviceStateService.ACTIVITY_STATE; import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_CONNECT_TIME; @@ -94,6 +96,7 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { private final ConcurrentMap sessions = new ConcurrentHashMap<>(); + private final ConcurrentMap sessionsById = new ConcurrentHashMap<>(); private final ConcurrentMap sessionNewEventsLocks = new ConcurrentHashMap<>(); private final Map sessionNewEvents = new HashMap<>(); private final ConcurrentMap> sessionEdgeEventChecks = new ConcurrentHashMap<>(); @@ -283,6 +286,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i destroySession(session); session.cleanUp(); sessions.remove(edgeId); + sessionsById.remove(session.getSessionId()); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); newEventLock.lock(); try { @@ -332,9 +336,15 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i TenantId tenantId = edge.getTenantId(); log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId); if (sessions.containsKey(edgeId)) { - destroySession(sessions.get(edgeId)); + EdgeGrpcSession existing = sessions.get(edgeId); + if (existing != null) { + log.info("[{}][{}] Replacing existing session [{}] for edge [{}]", tenantId, edgeGrpcSession.getSessionId(), existing.getSessionId(), edgeId); + destroySession(existing); + sessionsById.remove(existing.getSessionId()); + } } sessions.put(edgeId, edgeGrpcSession); + sessionsById.put(edgeGrpcSession.getSessionId(), edgeGrpcSession); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); newEventLock.lock(); try { @@ -492,9 +502,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private void onEdgeDisconnect(Edge edge, UUID sessionId) { EdgeId edgeId = edge.getId(); log.info("[{}][{}] edge disconnected!", edgeId, sessionId); - EdgeGrpcSession toRemove = sessions.get(edgeId); - if (toRemove.getSessionId().equals(sessionId)) { - toRemove = sessions.remove(edgeId); + EdgeGrpcSession current = sessions.get(edgeId); + if (current != null && current.getSessionId().equals(sessionId)) { + EdgeGrpcSession toRemove = sessions.remove(edgeId); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); newEventLock.lock(); try { @@ -503,6 +513,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i newEventLock.unlock(); } destroySession(toRemove); + sessionsById.remove(sessionId); TenantId tenantId = toRemove.getEdge().getTenantId(); save(tenantId, edgeId, ACTIVITY_STATE, false); long lastDisconnectTs = System.currentTimeMillis(); @@ -510,7 +521,18 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edge, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT); cancelScheduleEdgeEventsCheck(edgeId); } else { - log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId); + log.info("[{}] edge session [{}] is not current anymore. Attempting to destroy it by sessionId.", edgeId, sessionId); + EdgeGrpcSession stale = sessionsById.remove(sessionId); + if (stale != null) { + try { + destroySession(stale); + log.info("[{}][{}] Successfully destroyed stale session for edge [{}]", stale.getTenantId(), sessionId, edgeId); + } catch (Exception e) { + log.warn("[{}][{}] Failed to destroy stale session for edge [{}]", stale.getTenantId(), sessionId, edgeId, e); + } + } else { + log.debug("[{}] No session found by sessionId [{}] to destroy", edgeId, sessionId); + } } edgeIdServiceIdCache.evict(edgeId); } @@ -522,6 +544,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId()); zombieSessions.add(session); } + } catch (Exception e) { + log.warn("[{}][{}] Exception during session destroy for edge [{}] with session id [{}]", + session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId(), e); } } @@ -631,25 +656,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private void cleanupZombieSessions() { try { - List toRemove = new ArrayList<>(); - for (EdgeGrpcSession session : sessions.values()) { - if (session instanceof KafkaEdgeGrpcSession kafkaSession && - !kafkaSession.isConnected() && - kafkaSession.getConsumer() != null && - kafkaSession.getConsumer().getConsumer() != null && - !kafkaSession.getConsumer().getConsumer().isStopped()) { - toRemove.add(kafkaSession.getEdge().getId()); - } - } - for (EdgeId edgeId : toRemove) { - log.info("[{}] Destroying session for edge because edge is not connected", edgeId); - EdgeGrpcSession removed = sessions.get(edgeId); - if (removed instanceof KafkaEdgeGrpcSession kafkaSession) { - if (kafkaSession.destroy()) { - sessions.remove(edgeId); - } - } - } + tryToDestroyZombieSessions(getZombieSessions(sessions.values()), s -> sessions.remove(s.getEdge().getId())); + tryToDestroyZombieSessions(getZombieSessions(sessionsById.values()), s -> sessionsById.remove(s.getSessionId())); + zombieSessions.removeIf(zombie -> { if (zombie.destroy()) { log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].", @@ -665,4 +674,38 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i log.warn("Failed to cleanup kafka sessions", e); } } + + private List getZombieSessions(Collection sessions) { + List result = new ArrayList<>(); + for (EdgeGrpcSession session : sessions) { + if (isKafkaSessionAndZombie(session)) { + result.add(session); + } + } + return result; + } + + private void tryToDestroyZombieSessions(List sessionsToRemove, Function 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; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java index d165be33d4..63669a8e3d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java @@ -101,8 +101,21 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession { @Override public ListenableFuture processEdgeEvents() { + if (!isConnected() || isSyncInProgress() || isHighPriorityProcessing) { + log.warn("[{}][{}] Session is not ready (connected={}, syncInProgress={}, highPriority={}), skip starting edge event consumer", + tenantId, edge != null ? edge.getId() : null, isConnected(), isSyncInProgress(), isHighPriorityProcessing); + return Futures.immediateFuture(Boolean.FALSE); + } if (consumer == null || (consumer.getConsumer() != null && consumer.getConsumer().isStopped())) { try { + if (consumerExecutor != null && !consumerExecutor.isShutdown()) { + try { + consumerExecutor.shutdown(); + awaitConsumerTermination(); + } catch (Exception e) { + log.warn("[{}][{}] Failed to shutdown previous consumer executor", tenantId, edge.getId(), e); + } + } this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer")); this.consumer = QueueConsumerManager.>builder() .name("TB Edge events [" + edge.getId() + "]") @@ -133,6 +146,7 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession { public boolean destroy() { try { if (consumer != null) { + log.info("[{}][{}] Stopping edge event consumer...", tenantId, edge != null ? edge.getId() : null); consumer.stop(); } } catch (Exception e) { @@ -141,16 +155,25 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession { } consumer = null; try { - if (consumerExecutor != null) { + if (consumerExecutor != null && !consumerExecutor.isShutdown()) { consumerExecutor.shutdown(); + awaitConsumerTermination(); } } catch (Exception e) { - log.warn("[{}][{}] Failed to shutdown consumer executor", tenantId, edge.getId(), e); + log.warn("[{}][{}] Failed to shutdown edge event consumer executor", tenantId, edge.getId(), e); return false; } return true; } + private void awaitConsumerTermination() { + try { + consumerExecutor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS); + } catch (InterruptedException ie) { + log.warn("[{}][{}] Interrupted while awaiting consumer executor termination", tenantId, edge.getId()); + } + } + @Override public void cleanUp() { String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edge.getId()).getTopic(); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index dee288d257..57a78a9b38 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.thingsboard.common.util.JacksonUtil; @@ -27,6 +27,9 @@ import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasVersion; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -64,6 +67,7 @@ import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.state.DefaultDeviceStateService; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -407,4 +411,26 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { }); } + protected boolean isSaveRequired(HasVersion current, HasVersion updated) { + updated.setVersion(null); + return !updated.equals(current); + } + + protected > Optional generateUniqueNameIfDuplicateExists( + TenantId tenantId, I entityId, E entity, @Nullable E entityWithSameName) { + + if (entityWithSameName == null || entityWithSameName.getId().equals(entityId)) { + return Optional.empty(); + } + String currentName = entity.getName(); + String newEntityName = generateRandomAlphabeticString(currentName); + + log.warn("[{}] Entity with name '{}' already exists (id={}). Renaming to '{}'", tenantId, currentName, entityWithSameName.getId(), newEntityName); + return Optional.of(newEntityName); + } + + protected static String generateRandomAlphabeticString(String prefix) { + return prefix + "_" + StringUtils.randomAlphabetic(15); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java index b6b4dbed5b..76e151cba8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java @@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.util.Pair; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AssetId; @@ -52,22 +51,15 @@ public abstract class BaseAssetProcessor extends BaseEdgeProcessor { } else { asset.setId(assetId); } - String assetName = asset.getName(); - Asset assetByName = edgeCtx.getAssetService().findAssetByTenantIdAndName(tenantId, assetName); - if (assetByName != null && !assetByName.getId().equals(assetId)) { - assetName = assetName + "_" + StringUtils.randomAlphanumeric(15); - log.warn("[{}] Asset with name {} already exists. Renaming asset name to {}", - tenantId, asset.getName(), assetName); - assetNameUpdated = true; + if (isSaveRequired(assetById, asset)) { + assetNameUpdated = updateAssetNameIfDuplicateExists(tenantId, assetId, asset); + setCustomerId(tenantId, created ? null : assetById.getCustomerId(), asset, assetUpdateMsg); + assetValidator.validate(asset, Asset::getTenantId); + if (created) { + asset.setId(assetId); + } + edgeCtx.getAssetService().saveAsset(asset, false); } - asset.setName(assetName); - setCustomerId(tenantId, created ? null : assetById.getCustomerId(), asset, assetUpdateMsg); - - assetValidator.validate(asset, Asset::getTenantId); - if (created) { - asset.setId(assetId); - } - edgeCtx.getAssetService().saveAsset(asset, false); } catch (Exception e) { log.error("[{}] Failed to process asset update msg [{}]", tenantId, assetUpdateMsg, e); throw e; @@ -77,6 +69,15 @@ public abstract class BaseAssetProcessor extends BaseEdgeProcessor { return Pair.of(created, assetNameUpdated); } + private boolean updateAssetNameIfDuplicateExists(TenantId tenantId, AssetId assetId, Asset asset) { + Asset assetByName = edgeCtx.getAssetService().findAssetByTenantIdAndName(tenantId, asset.getName()); + + return generateUniqueNameIfDuplicateExists(tenantId, assetId, asset, assetByName).map(uniqueName -> { + asset.setName(uniqueName); + return true; + }).orElse(false); + } + protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, Asset asset, AssetUpdateMsg assetUpdateMsg); protected void deleteAsset(TenantId tenantId, AssetId assetId) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java index 52b33e5297..8a202289f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java @@ -58,19 +58,18 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor { dashboard.setAssignedCustomers(dashboardById.getAssignedCustomers()); } - dashboardValidator.validate(dashboard, Dashboard::getTenantId); - if (created) { - dashboard.setId(dashboardId); + if (isSaveRequired(dashboardById, dashboard)) { + dashboardValidator.validate(dashboard, Dashboard::getTenantId); + if (created) { + dashboard.setId(dashboardId); + } + dashboard = edgeCtx.getDashboardService().saveDashboard(dashboard, false); } - - Dashboard savedDashboard = edgeCtx.getDashboardService().saveDashboard(dashboard, false); - - updateDashboardAssignments(tenantId, dashboardById, savedDashboard, newAssignedCustomers); - + updateDashboardAssignments(tenantId, customerId, dashboardById, dashboard, newAssignedCustomers); return created; } - private void updateDashboardAssignments(TenantId tenantId, Dashboard dashboardById, Dashboard savedDashboard, Set newAssignedCustomers) { + private void updateDashboardAssignments(TenantId tenantId, CustomerId edgeCustomerId, Dashboard dashboardById, Dashboard savedDashboard, Set newAssignedCustomers) { Set currentAssignedCustomers = new HashSet<>(); if (dashboardById != null) { if (dashboardById.getAssignedCustomers() != null) { @@ -78,7 +77,7 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor { } } - newAssignedCustomers = filterNonExistingCustomers(tenantId, currentAssignedCustomers, newAssignedCustomers); + newAssignedCustomers = filterNonExistingCustomers(tenantId, edgeCustomerId, currentAssignedCustomers, newAssignedCustomers); Set addedCustomerIds = new HashSet<>(); Set removedCustomerIds = new HashSet<>(); @@ -114,6 +113,6 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor { } } - protected abstract Set filterNonExistingCustomers(TenantId tenantId, Set currentAssignedCustomers, Set newAssignedCustomers); + protected abstract Set filterNonExistingCustomers(TenantId tenantId, CustomerId customerId, Set currentAssignedCustomers, Set newAssignedCustomers); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java index 522c2ba477..b38a9d618e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; @@ -36,8 +37,10 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; +import java.util.HashSet; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; @Slf4j @Component @@ -116,14 +119,24 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor implements Da } @Override - protected Set filterNonExistingCustomers(TenantId tenantId, Set currentAssignedCustomers, Set newAssignedCustomers) { - newAssignedCustomers.addAll(currentAssignedCustomers); - return newAssignedCustomers; + protected Set filterNonExistingCustomers(TenantId tenantId, CustomerId edgeCustomerId, Set currentAssignedCustomers, Set newAssignedCustomers) { + boolean edgeCustomerPresentInNewAssignments = newAssignedCustomers.stream() + .map(ShortCustomerInfo::getCustomerId) + .anyMatch(edgeCustomerId::equals); + + if (edgeCustomerPresentInNewAssignments) { + Set result = new HashSet<>(newAssignedCustomers); + result.addAll(currentAssignedCustomers); + return result; + } else { + return currentAssignedCustomers.stream() + .filter(info -> !edgeCustomerId.equals(info.getCustomerId())) + .collect(Collectors.toSet()); + } } @Override public EdgeEventType getEdgeEventType() { return EdgeEventType.DASHBOARD; } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java index 3f516de44b..d948955503 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java @@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.util.Pair; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; @@ -54,23 +53,17 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { } else { device.setId(deviceId); } - String deviceName = device.getName(); - Device deviceByName = edgeCtx.getDeviceService().findDeviceByTenantIdAndName(tenantId, deviceName); - if (deviceByName != null && !deviceByName.getId().equals(deviceId)) { - deviceName = deviceName + "_" + StringUtils.randomAlphabetic(15); - log.warn("[{}] Device with name {} already exists. Renaming device name to {}", - tenantId, device.getName(), deviceName); - deviceNameUpdated = true; - } - device.setName(deviceName); - setCustomerId(tenantId, created ? null : deviceById.getCustomerId(), device, deviceUpdateMsg); + if (isSaveRequired(deviceById, device)) { + deviceNameUpdated = updateDeviceNameIfDuplicateExists(tenantId, deviceId, device); + setCustomerId(tenantId, created ? null : deviceById.getCustomerId(), device, deviceUpdateMsg); - deviceValidator.validate(device, Device::getTenantId); - if (created) { - device.setId(deviceId); + deviceValidator.validate(device, Device::getTenantId); + if (created) { + device.setId(deviceId); + } + Device savedDevice = edgeCtx.getDeviceService().saveDevice(device, false); + edgeCtx.getClusterService().onDeviceUpdated(savedDevice, created ? null : device); } - Device savedDevice = edgeCtx.getDeviceService().saveDevice(device, false); - edgeCtx.getClusterService().onDeviceUpdated(savedDevice, created ? null : device); } catch (Exception e) { log.error("[{}] Failed to process device update msg [{}]", tenantId, deviceUpdateMsg, e); throw e; @@ -80,6 +73,15 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { return Pair.of(created, deviceNameUpdated); } + private boolean updateDeviceNameIfDuplicateExists(TenantId tenantId, DeviceId deviceId, Device device) { + Device deviceByName = edgeCtx.getDeviceService().findDeviceByTenantIdAndName(tenantId, device.getName()); + + return generateUniqueNameIfDuplicateExists(tenantId, deviceId, device, deviceByName).map(uniqueName -> { + device.setName(uniqueName); + return true; + }).orElse(false); + } + protected void updateDeviceCredentials(TenantId tenantId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) { DeviceCredentials deviceCredentials = JacksonUtil.fromString(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class, true); if (deviceCredentials == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java index 97f712c5ac..6346089df5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java @@ -21,7 +21,9 @@ import org.springframework.data.util.Pair; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; @@ -50,25 +52,28 @@ public abstract class BaseEntityViewProcessor extends BaseEdgeProcessor { } else { entityView.setId(entityViewId); } - String entityViewName = entityView.getName(); - EntityView entityViewByName = edgeCtx.getEntityViewService().findEntityViewByTenantIdAndName(tenantId, entityViewName); - if (entityViewByName != null && !entityViewByName.getId().equals(entityViewId)) { - entityViewName = entityViewName + "_" + StringUtils.randomAlphanumeric(15); - log.warn("[{}] Entity view with name {} already exists. Renaming entity view name to {}", - tenantId, entityView.getName(), entityViewName); - entityViewNameUpdated = true; - } - entityView.setName(entityViewName); - setCustomerId(tenantId, created ? null : entityViewById.getCustomerId(), entityView, entityViewUpdateMsg); + if (isSaveRequired(entityViewById, entityView)) { + entityViewNameUpdated = updateEntityViewNameIfDuplicateExists(tenantId, entityViewId, entityView); + setCustomerId(tenantId, created ? null : entityViewById.getCustomerId(), entityView, entityViewUpdateMsg); - entityViewValidator.validate(entityView, EntityView::getTenantId); - if (created) { - entityView.setId(entityViewId); + entityViewValidator.validate(entityView, EntityView::getTenantId); + if (created) { + entityView.setId(entityViewId); + } + edgeCtx.getEntityViewService().saveEntityView(entityView, false); } - edgeCtx.getEntityViewService().saveEntityView(entityView, false); return Pair.of(created, entityViewNameUpdated); } + private boolean updateEntityViewNameIfDuplicateExists(TenantId tenantId, EntityViewId entityViewId, EntityView entityView) { + EntityView entityViewByName = edgeCtx.getEntityViewService().findEntityViewByTenantIdAndName(tenantId, entityView.getName()); + + return generateUniqueNameIfDuplicateExists(tenantId, entityViewId, entityView, entityViewByName).map(uniqueName -> { + entityView.setName(uniqueName); + return true; + }).orElse(false); + } + protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, EntityView entityView, EntityViewUpdateMsg entityViewUpdateMsg); protected void deleteEntityView(TenantId tenantId, EntityViewId entityViewId) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java index fc24588f5b..ede8c94af0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -22,7 +22,6 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; @@ -57,27 +56,18 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { } else { user.setId(userId); } + if (isSaveRequired(userById, user)) { + userEmailUpdated = updateUserEmailIfDuplicateExists(tenantId, userId, user); + setCustomerId(tenantId, isCreated ? null : userById.getCustomerId(), user, userUpdateMsg); - String userEmail = user.getEmail(); - User existing = edgeCtx.getUserService().findUserByTenantIdAndEmail(tenantId, user.getEmail()); + userValidator.validate(user, User::getTenantId); - if (existing != null && !existing.getId().equals(user.getId())) { - String[] splitEmail = userEmail.split("@"); - userEmail = splitEmail[0] + "_" + StringUtils.randomAlphanumeric(15) + "@" + splitEmail[1]; - log.warn("[{}] User with email {} already exists. Renaming User email to {}", - tenantId, user.getEmail(), userEmail); - userEmailUpdated = true; - } - user.setEmail(userEmail); - setCustomerId(tenantId, isCreated ? null : userById.getCustomerId(), user, userUpdateMsg); - - userValidator.validate(user, User::getTenantId); + if (isCreated) { + user.setId(userId); + } - if (isCreated) { - user.setId(userId); + edgeCtx.getUserService().saveUser(tenantId, user, false); } - - edgeCtx.getUserService().saveUser(tenantId, user, false); } catch (Exception e) { log.error("[{}] Failed to process user update msg [{}]", tenantId, userUpdateMsg, e); throw e; @@ -86,6 +76,20 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { return Pair.of(isCreated, userEmailUpdated); } + private boolean updateUserEmailIfDuplicateExists(TenantId tenantId, UserId userId, User user) { + String email = user.getEmail(); + User userByEmail = edgeCtx.getUserService().findUserByTenantIdAndEmail(tenantId, email); + + if (userByEmail != null && !userByEmail.getId().equals(user.getId())) { + String[] splitEmail = email.split("@"); + String newEmail = splitEmail[0] + "_" + StringUtils.randomAlphanumeric(15) + "@" + splitEmail[1]; + log.warn("[{}] User with email {} already exists. Renaming User email to {}", tenantId, user.getEmail(), newEmail); + user.setEmail(newEmail); + return true; + } + return false; + } + protected void deleteUserAndPushEntityDeletedEventToRuleEngine(TenantId tenantId, UserId userId) { deleteUserAndPushEntityDeletedEventToRuleEngine(tenantId, userId, null); } diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index f39769526a..6ef6b239e5 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -15,24 +15,18 @@ */ package org.thingsboard.server.service.query; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; -import org.springframework.web.context.request.async.DeferredResult; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.KvUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -40,6 +34,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.AvailableEntityKeys; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.EntityCountQuery; @@ -56,16 +51,13 @@ import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; -import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.sql.query.EntityKeyMapping; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.executors.DbCallbackExecutorService; -import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.model.SecurityUser; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -73,9 +65,10 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.Futures.immediateFuture; + @Service @Slf4j @TbCoreComponent @@ -138,20 +131,12 @@ public class DefaultEntityQueryService implements EntityQueryService { } private void resolveDynamicValue(DynamicValue dynamicValue, SecurityUser user, FilterPredicateType predicateType) { - EntityId entityId; - switch (dynamicValue.getSourceType()) { - case CURRENT_TENANT: - entityId = user.getTenantId(); - break; - case CURRENT_CUSTOMER: - entityId = user.getCustomerId(); - break; - case CURRENT_USER: - entityId = user.getId(); - break; - default: - throw new RuntimeException("Not supported operation for source type: {" + dynamicValue.getSourceType() + "}"); - } + EntityId entityId = switch (dynamicValue.getSourceType()) { + case CURRENT_TENANT -> user.getTenantId(); + case CURRENT_CUSTOMER -> user.getCustomerId(); + case CURRENT_USER -> user.getId(); + default -> throw new RuntimeException("Not supported operation for source type: {" + dynamicValue.getSourceType() + "}"); + }; try { Optional valueOpt = attributesService.find(user.getTenantId(), entityId, @@ -242,101 +227,51 @@ public class DefaultEntityQueryService implements EntityQueryService { } @Override - public DeferredResult getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, - boolean isTimeseries, boolean isAttributes, String attributesScope) { - final DeferredResult response = new DeferredResult<>(); + public ListenableFuture getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, + boolean isTimeseries, boolean isAttributes, AttributeScope scope) { if (!isAttributes && !isTimeseries) { - replyWithEmptyResponse(response); - return response; + return immediateFuture(AvailableEntityKeys.none()); } - List ids = this.findEntityDataByQuery(securityUser, query).getData().stream() + List ids = findEntityDataByQuery(securityUser, query).getData().stream() .map(EntityData::getEntityId) - .collect(Collectors.toList()); + .toList(); if (ids.isEmpty()) { - replyWithEmptyResponse(response); - return response; + return immediateFuture(AvailableEntityKeys.none()); } Set types = ids.stream().map(EntityId::getEntityType).collect(Collectors.toSet()); - final ListenableFuture> timeseriesKeysFuture; - final ListenableFuture> attributesKeysFuture; + ListenableFuture> timeseriesKeysFuture; + ListenableFuture> attributesKeysFuture; if (isTimeseries) { - timeseriesKeysFuture = dbCallbackExecutor.submit(() -> timeseriesService.findAllKeysByEntityIds(tenantId, ids)); + timeseriesKeysFuture = timeseriesService.findAllKeysByEntityIdsAsync(tenantId, ids); } else { - timeseriesKeysFuture = null; + timeseriesKeysFuture = immediateFuture(Collections.emptyList()); } if (isAttributes) { Map> typesMap = ids.stream().collect(Collectors.groupingBy(EntityId::getEntityType)); List>> futures = new ArrayList<>(typesMap.size()); - typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIds(tenantId, entityIds, attributesScope)))); + typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIds(tenantId, entityIds, scope)))); attributesKeysFuture = Futures.transform(Futures.allAsList(futures), lists -> { if (CollectionUtils.isEmpty(lists)) { return Collections.emptyList(); } - return lists.stream().flatMap(List::stream).distinct().sorted().collect(Collectors.toList()); - }, dbCallbackExecutor); - } else { - attributesKeysFuture = null; - } - - if (isTimeseries && isAttributes) { - Futures.whenAllComplete(timeseriesKeysFuture, attributesKeysFuture).run(() -> { - try { - replyWithResponse(response, types, timeseriesKeysFuture.get(), attributesKeysFuture.get()); - } catch (Exception e) { - log.error("Failed to fetch timeseries and attributes keys!", e); - AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR); - } + return lists.stream().flatMap(List::stream).distinct().sorted().toList(); }, dbCallbackExecutor); - } else if (isTimeseries) { - addCallback(timeseriesKeysFuture, keys -> replyWithResponse(response, types, keys, null), - error -> { - log.error("Failed to fetch timeseries keys!", error); - AccessValidator.handleError(error, response, HttpStatus.INTERNAL_SERVER_ERROR); - }); } else { - addCallback(attributesKeysFuture, keys -> replyWithResponse(response, types, null, keys), - error -> { - log.error("Failed to fetch attributes keys!", error); - AccessValidator.handleError(error, response, HttpStatus.INTERNAL_SERVER_ERROR); - }); + attributesKeysFuture = immediateFuture(Collections.emptyList()); } - return response; - } - - private void replyWithResponse(DeferredResult response, Set types, List timeseriesKeys, List attributesKeys) { - ObjectNode json = JacksonUtil.newObjectNode(); - addItemsToArrayNode(json.putArray("entityTypes"), types); - addItemsToArrayNode(json.putArray("timeseries"), timeseriesKeys); - addItemsToArrayNode(json.putArray("attribute"), attributesKeys); - response.setResult(new ResponseEntity<>(json, HttpStatus.OK)); - } - private void replyWithEmptyResponse(DeferredResult response) { - replyWithResponse(response, Collections.emptySet(), Collections.emptyList(), Collections.emptyList()); - } - - private void addItemsToArrayNode(ArrayNode arrayNode, Collection collection) { - if (!CollectionUtils.isEmpty(collection)) { - collection.forEach(item -> arrayNode.add(item.toString())); - } - } - - private void addCallback(ListenableFuture> future, Consumer> success, Consumer error) { - Futures.addCallback(future, new FutureCallback>() { - @Override - public void onSuccess(@Nullable List keys) { - success.accept(keys); - } - - @Override - public void onFailure(Throwable t) { - error.accept(t); - } - }, dbCallbackExecutor); + return Futures.whenAllComplete(timeseriesKeysFuture, attributesKeysFuture) + .call(() -> { + try { + return new AvailableEntityKeys(types, Futures.getDone(timeseriesKeysFuture), Futures.getDone(attributesKeysFuture)); + } catch (ExecutionException e) { + throw new ThingsboardException(e.getCause(), ThingsboardErrorCode.DATABASE); + } + }, dbCallbackExecutor); } } diff --git a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java index 78ea2519fd..ac6553d738 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java @@ -15,13 +15,14 @@ */ package org.thingsboard.server.service.query; -import org.springframework.http.ResponseEntity; -import org.springframework.web.context.request.async.DeferredResult; +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.AvailableEntityKeys; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -37,7 +38,7 @@ public interface EntityQueryService { long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query); - DeferredResult getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, - boolean isTimeseries, boolean isAttributes, String attributesScope); + ListenableFuture getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, + boolean isTimeseries, boolean isAttributes, AttributeScope scope); } diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java index 28ec9af4b2..43d243eeb8 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java @@ -75,7 +75,7 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements ActionType actionType = resource.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = resource.getTenantId(); try { - if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType())) { + if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType()) && resource.getId() == null) { toLwm2mResource(resource); } else if (resource.getResourceKey() == null) { resource.setResourceKey(resource.getFileName()); diff --git a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java index 2e6e43e7bf..6ae186b83f 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java @@ -19,8 +19,8 @@ import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceDeleteResult; -import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.lwm2m.LwM2mObject; diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmStatusSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmStatusSubCtx.java index b3cd136726..6b3d38e8eb 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmStatusSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmStatusSubCtx.java @@ -95,6 +95,9 @@ public class TbAlarmStatusSubCtx extends TbAbstractSubCtx { private void handleAlarmStatusSubscriptionUpdate(TbSubscription sub, AlarmSubscriptionUpdate subscriptionUpdate) { try { AlarmInfo alarm = subscriptionUpdate.getAlarm(); + if (!alarm.getOriginator().equals(subscription.getEntityId())) { + return; + } Set alarmsIds = subscription.getAlarmIds(); if (alarmsIds.contains(alarm.getId().getId())) { if (!subscription.matches(alarm) || subscriptionUpdate.isAlarmDeleted()) { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java index 5d634c4178..70e90b2636 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.sync.ie.exporting.impl; +import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; @@ -24,6 +25,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -170,6 +172,13 @@ public class DefaultEntityExportService { + if (rule.getDashboardId() != null) { + rule.setDashboardId(getExternalIdOrElseInternal(ctx, rule.getDashboardId())); + } + }); + } }); return calculatedFields; } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index c95fffc205..61aca6eb8f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -22,6 +22,7 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; @@ -35,6 +36,7 @@ import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -338,6 +340,13 @@ public abstract class BaseEntityImportService { + if (rule.getDashboardId() != null) { + rule.setDashboardId(idProvider.getInternalId(rule.getDashboardId(), ctx.isFinalImportAttempt())); + } + }); + } }).toList(); for (CalculatedField existingField : existing) { diff --git a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java index 3c5ab63cd2..cb82e9b133 100644 --- a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java @@ -150,10 +150,10 @@ public class DefaultUpdateService implements UpdateService { .build()); } ObjectNode edgeRequest = JacksonUtil.newObjectNode().put(VERSION_PARAM, version); - String edgeInstallVersion = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/installMapping", new HttpEntity<>(edgeRequest.toString(), headers), String.class); - if (edgeInstallVersion != null) { - edgeInstallInstructionsService.setAppVersion(edgeInstallVersion); - edgeUpgradeInstructionsService.setAppVersion(edgeInstallVersion); + String edgePlatformVersion = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/installMapping", new HttpEntity<>(edgeRequest.toString(), headers), String.class); + if (edgePlatformVersion != null) { + edgeInstallInstructionsService.setPlatformEdgeVersion(edgePlatformVersion); + edgeUpgradeInstructionsService.setPlatformEdgeVersion(edgePlatformVersion); } EdgeUpgradeMessage edgeUpgradeMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/upgradeMapping", new HttpEntity<>(edgeRequest.toString(), headers), EdgeUpgradeMessage.class); if (edgeUpgradeMessage != null) { diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java index 97d53ac252..74bbdd8a34 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java @@ -15,9 +15,7 @@ */ package org.thingsboard.server.utils; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; +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; @@ -25,6 +23,7 @@ import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric import org.thingsboard.server.common.data.id.EntityId; 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; @@ -48,20 +47,13 @@ import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalcul import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; + +import static org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry.DEFAULT_VERSION; public class CalculatedFieldArgumentUtils { - public static ListenableFuture transformSingleValueArgument(ListenableFuture> kvEntryFuture) { - return Futures.transform(kvEntryFuture, CalculatedFieldArgumentUtils::transformSingleValueArgument, MoreExecutors.directExecutor()); - } - - public static ArgumentEntry transformSingleValueArgument(Optional kvEntry) { - if (kvEntry.isPresent() && kvEntry.get().getValue() != null) { - return ArgumentEntry.createSingleValueArgument(kvEntry.get()); - } else { - return new SingleValueArgumentEntry(); - } + public static ArgumentEntry transformSingleValueArgument(@NonNull KvEntry kvEntry) { + return kvEntry.getValue() != null ? ArgumentEntry.createSingleValueArgument(kvEntry) : new SingleValueArgumentEntry(); } public static ArgumentEntry transformTsRollingArgument(List tsRolling, int limit, long argTimeWindow) { @@ -94,7 +86,7 @@ public class CalculatedFieldArgumentUtils { return new EntityAggregationArgumentEntry(aggIntervals); } - public static KvEntry createDefaultKvEntry(Argument argument) { + private static KvEntry createDefaultKvEntry(Argument argument) { String key = argument.getRefEntityKey().getKey(); String defaultValue = argument.getDefaultValue(); if (StringUtils.isBlank(defaultValue)) { @@ -109,9 +101,12 @@ public class CalculatedFieldArgumentUtils { return new StringDataEntry(key, defaultValue); } + public static TsKvEntry createDefaultTsKvEntry(Argument argument, long ts) { + return new BasicTsKvEntry(ts, createDefaultKvEntry(argument), DEFAULT_VERSION); + } + public static AttributeKvEntry createDefaultAttributeEntry(Argument argument, long ts) { - KvEntry kvEntry = createDefaultKvEntry(argument); - return new BaseAttributeKvEntry(kvEntry, ts, 0L); + return new BaseAttributeKvEntry(createDefaultKvEntry(argument), ts, DEFAULT_VERSION); } public static CalculatedFieldState createStateByType(CalculatedFieldCtx ctx, EntityId entityId) { diff --git a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java index fb6125bc0b..172a3e8e2d 100644 --- a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java @@ -73,7 +73,7 @@ public class LwM2mObjectModelUtils { try { List objectModels = ddfFileParser.parse(new ByteArrayInputStream(resource.getData()), resource.getSearchText()); - if (objectModels.size() == 0) { + if (objectModels.isEmpty()) { return null; } else { ObjectModel obj = objectModels.get(0); @@ -95,7 +95,7 @@ public class LwM2mObjectModelUtils { resources.add(lwM2MResourceObserve); } }); - if (isSave || resources.size() > 0) { + if (isSave || !resources.isEmpty()) { instance.setResources(resources.toArray(LwM2mResourceObserve[]::new)); lwM2mObject.setInstances(new LwM2mInstance[]{instance}); return lwM2mObject; diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 4ffa7f31d2..d3d04bcea0 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -542,7 +542,7 @@ actors: check_interval: "${ACTORS_CALCULATED_FIELDS_CHECK_INTERVAL_SEC:60}" alarms: # Interval in seconds to re-evaluate Alarm rules that have a time schedule. 2 minutes by default. - reevaluation_interval: "${ACTORS_ALARMS_REEVALUATION_INTERVAL_SEC:120}" + reevaluation_interval: "${ACTORS_ALARMS_REEVALUATION_INTERVAL_SEC:60}" debug: settings: diff --git a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java index 2ac5d59b3a..5f91dab190 100644 --- a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmC import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NoDataFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate.NumericOperation; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate; @@ -475,6 +476,61 @@ public class AlarmRulesTest extends AbstractControllerTest { }); } + @Test + public void testCreateAlarm_noDataPredicate() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + long majorNoDataDuration = 3L; + long criticalNoDataDuration = 10L; + + SimpleAlarmConditionExpression majorExpression = new SimpleAlarmConditionExpression(); + AlarmConditionFilter majorFilter = new AlarmConditionFilter(); + majorFilter.setArgument("temperature"); + majorFilter.setValueType(EntityKeyValueType.NUMERIC); + majorFilter.setPredicates(List.of( + new NumericFilterPredicate(NumericOperation.GREATER, new AlarmConditionValue<>(25.0, null)), + new NoDataFilterPredicate(TimeUnit.SECONDS, new AlarmConditionValue(majorNoDataDuration, null)) + )); + majorExpression.setFilters(List.of(majorFilter)); + + SimpleAlarmConditionExpression criticalExpression = new SimpleAlarmConditionExpression(); + AlarmConditionFilter criticalFilter = new AlarmConditionFilter(); + criticalFilter.setArgument("temperature"); + criticalFilter.setValueType(EntityKeyValueType.NUMERIC); + criticalFilter.setPredicates(List.of( + new NumericFilterPredicate(NumericOperation.GREATER, new AlarmConditionValue<>(25.0, null)), + new NoDataFilterPredicate(TimeUnit.SECONDS, new AlarmConditionValue(criticalNoDataDuration, null)) + )); + criticalExpression.setFilters(List.of(criticalFilter)); + + Map createRules = Map.of( + AlarmSeverity.MAJOR, new Condition(majorExpression, null, null), + AlarmSeverity.CRITICAL, new Condition(criticalExpression, null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "No Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":50}"); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isSeverityUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + @Test public void testChangeAlarmType() throws Exception { Argument temperatureArgument = new Argument(); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 51d77bd134..80e79280df 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -84,6 +84,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.StringUtils; @@ -93,6 +94,7 @@ import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; @@ -1207,8 +1209,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { Map statesMap = (Map) ReflectionTestUtils.getField(processor, "states"); Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { CalculatedFieldState calculatedFieldState = statesMap.get(cfId); - boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs() - < System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval); + boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs() < + System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval); log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady); return isReady; }); @@ -1399,7 +1401,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected List findJobs(List types, List entities) throws Exception { return doGetTypedWithPageLink("/api/jobs?types=" + types.stream().map(Enum::name).collect(Collectors.joining(",")) + - "&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&", + "&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&", new TypeReference>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); } @@ -1413,12 +1415,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected void postTelemetry(EntityId entityId, String payload) throws Exception { doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + - "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); } protected void postAttributes(EntityId entityId, AttributeScope scope, String payload) throws Exception { doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + - "/attributes/" + scope, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); + "/attributes/" + scope, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); } protected CalculatedField saveCalculatedField(CalculatedField calculatedField) { @@ -1427,7 +1429,24 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected PageData getEntityCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception { return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" + - (type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink); + (type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink); + } + + protected PageData getCalculatedFieldNames(CalculatedFieldType type, PageLink pageLink) throws Exception { + return doGetTypedWithPageLink("/api/calculatedFields/names?type=" + type + "&", + new TypeReference>() {}, pageLink); + } + + protected List getCalculatedFields(CalculatedFieldType type, + EntityType entityType, + List entities, + List names) throws Exception { + return doGetTypedWithPageLink("/api/calculatedFields?type=" + type + "&" + + (entityType != null ? "entityType=" + entityType + "&" : "") + + (entities != null ? "entities=" + String.join(",", + entities.stream().map(UUID::toString).toList()) + "&" : "") + + (names != null ? names.stream().map(name -> "name=" + name + "&").collect(Collectors.joining("")) : ""), + new TypeReference>() {}, new PageLink(10)).getData(); } protected PageData getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java index 635fc747e5..33f6bfa250 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.core.type.TypeReference; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -56,9 +55,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.Comparator; import java.util.List; import java.util.Map; -import java.util.UUID; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; @@ -295,23 +292,6 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { assertThat(names.getData()).containsOnly(deviceCalculatedField.getName()); } - private PageData getCalculatedFieldNames(CalculatedFieldType type, PageLink pageLink) throws Exception { - return doGetTypedWithPageLink("/api/calculatedFields/names?type=" + type + "&", - new TypeReference>() {}, pageLink); - } - - private List getCalculatedFields(CalculatedFieldType type, - EntityType entityType, - List entities, - List names) throws Exception { - return doGetTypedWithPageLink("/api/calculatedFields?type=" + type + "&" + - (entityType != null ? "entityType=" + entityType + "&" : "") + - (entities != null ? "entities=" + String.join(",", - entities.stream().map(UUID::toString).toList()) + "&" : "") + - (names != null ? names.stream().map(name -> "name=" + name + "&").collect(Collectors.joining("")) : ""), - new TypeReference>() {}, new PageLink(10)).getData(); - } - @Test public void testDeleteCalculatedField() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java index fdf54d0109..724b1c622f 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java @@ -321,9 +321,10 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { "\n" + " # Environment variables\n" + " environment:\n" + - " - host=host.docker.internal\n" + - " - port=1883\n" + - " - accessToken=" + credentials.getCredentialsId() + "\n" + + " - TB_GW_HOST=host.docker.internal\n" + + " - TB_GW_PORT=1883\n" + + " - TB_GW_SECURITY_TYPE=accessToken\n" + + " - TB_GW_ACCESS_TOKEN=" + credentials.getCredentialsId() + "\n" + "\n" + " # Volumes bind\n" + " volumes:\n" + diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index faefab4cb4..f1d3ef1d52 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -1278,7 +1278,7 @@ public class EdgeControllerTest extends AbstractControllerTest { @Test public void testGetEdgeUpgradeInstructions() throws Exception { - // UpdateInfo config is updating from Thingsboard Update server + // UpdateInfo config is updating from the Thingsboard Update server HashMap upgradeInfoHashMap = new HashMap<>(); upgradeInfoHashMap.put("3.6.0", new EdgeUpgradeInfo(true, "3.6.1")); upgradeInfoHashMap.put("3.6.1", new EdgeUpgradeInfo(true, "3.6.2")); @@ -1301,35 +1301,31 @@ public class EdgeControllerTest extends AbstractControllerTest { // Test 3.5.0 Edge - upgrade not available String body = "{\"edgeVersion\": \"V_3_5_0\"}"; doPostAsync("/api/plugins/telemetry/EDGE/" + savedEdge.getId().getId() + "/attributes/SERVER_SCOPE", body, String.class, status().isOk()); - edgeUpgradeInstructionsService.setAppVersion("3.6.0"); + edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.0"); Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); - edgeUpgradeInstructionsService.setAppVersion("3.6.2"); + edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.2"); Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); - edgeUpgradeInstructionsService.setAppVersion("3.6.2.7"); + edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.2.7"); Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); // Test 3.6.0 Edge - upgrade available body = "{\"edgeVersion\": \"V_3_6_0\"}"; doPostAsync("/api/plugins/telemetry/EDGE/" + savedEdge.getId().getId() + "/attributes/SERVER_SCOPE", body, String.class, status().isOk()); - edgeUpgradeInstructionsService.setAppVersion("3.6.0"); + edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.0"); Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); - edgeUpgradeInstructionsService.setAppVersion("3.6.1.5"); + edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.1.5"); Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); - edgeUpgradeInstructionsService.setAppVersion("3.6.1.6-SNAPSHOT"); - Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); - edgeUpgradeInstructionsService.setAppVersion("3.6.2"); + edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.2"); Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); // Test 3.6.1 Edge - upgrade available body = "{\"edgeVersion\": \"V_3_6_1\"}"; doPostAsync("/api/plugins/telemetry/EDGE/" + savedEdge.getId().getId() + "/attributes/SERVER_SCOPE", body, String.class, status().isOk()); - edgeUpgradeInstructionsService.setAppVersion("3.6.1"); + edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.1"); Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); - edgeUpgradeInstructionsService.setAppVersion("3.6.2"); - Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); - edgeUpgradeInstructionsService.setAppVersion("3.6.2-SNAPSHOT"); + edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.2"); Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); - edgeUpgradeInstructionsService.setAppVersion("3.6.2.6"); + edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.2.6"); Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId())); } diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java index 91be3f4744..c3f11ce8d8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java @@ -22,6 +22,7 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Device; @@ -41,6 +42,7 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.edqs.util.EdqsRocksDb; import java.util.ArrayList; import java.util.Collections; @@ -62,8 +64,11 @@ public class EdqsControllerTest extends AbstractControllerTest { @Autowired private JdbcTemplate jdbcTemplate; + @MockitoBean + private EdqsRocksDb edqsRocksDb; + @Before - public void beforeEdqsControllerTest() throws Exception { + public void before() throws Exception { loginTenantAdmin(); } diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index 655ab417e6..dca25a83a1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -19,7 +19,6 @@ import org.assertj.core.api.ThrowingConsumer; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.edqs.EdqsState; import org.thingsboard.server.common.data.edqs.EdqsState.EdqsApiMode; @@ -33,7 +32,6 @@ import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.edqs.util.EdqsRocksDb; import org.thingsboard.server.queue.discovery.DiscoveryService; import java.util.concurrent.TimeUnit; @@ -59,9 +57,6 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest { @Autowired private DiscoveryService discoveryService; - @MockBean // so that we don't do backup for tests - private EdqsRocksDb edqsRocksDb; - @Before public void before() { await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.getState().isApiEnabled()); diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 775df230b6..e32a2e8086 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -23,6 +23,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.ResultActions; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.thingsboard.common.util.JacksonUtil; @@ -71,6 +72,7 @@ import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.edqs.util.EdqsRocksDb; import java.util.ArrayList; import java.util.Arrays; @@ -102,6 +104,9 @@ public class EntityQueryControllerTest extends AbstractControllerTest { @Autowired private QueueStatsService queueStatsService; + @MockitoBean + private EdqsRocksDb edqsRocksDb; + @Before public void beforeTest() throws Exception { loginSysAdmin(); @@ -436,7 +441,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityTypeFilter assetTypeFilter = new EntityTypeFilter(); assetTypeFilter.setEntityType(EntityType.ASSET); - AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields); + AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields); PageData alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); List retrievedAlarmTypes = alarmPageData.getData().stream().map(AlarmData::getType).toList(); @@ -446,8 +451,8 @@ public class EntityQueryControllerTest extends AbstractControllerTest { KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); List keyFilters = Collections.singletonList(nameFilter); - AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields); - PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { + AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields); + PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { }); Assert.assertEquals(1, filteredAssetAlamData.getTotalElements()); } @@ -509,16 +514,16 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityTypeFilter assetTypeFilter = new EntityTypeFilter(); assetTypeFilter.setEntityType(EntityType.ASSET); - AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, Collections.emptyList()); + AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, Collections.emptyList()); - PageData alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); + PageData alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); List retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList(); assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes); KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); List keyFilters = Collections.singletonList(nameFilter); - AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList()); - PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { + AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList()); + PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { }); Assert.assertEquals(1, filteredAssetAlamData.getTotalElements()); } @@ -574,7 +579,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityTypeFilter deviceTypeFilter = new EntityTypeFilter(); deviceTypeFilter.setEntityType(EntityType.DEVICE); - AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields); + AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields); PageData alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10); List retrievedAlarmTemps = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList(); @@ -1291,7 +1296,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { findByQueryAndCheck(query, 0); } - private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer checkFunction) throws Exception { + private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer checkFunction) throws Exception { await() .alias("data by query") .atMost(30, TimeUnit.SECONDS) diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java index d88aaa2756..0875551112 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java @@ -25,11 +25,12 @@ import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockPart; import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; @@ -47,15 +48,14 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; -import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; import java.util.Base64; -import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; @@ -64,7 +64,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest public class TbResourceControllerTest extends AbstractControllerTest { - private IdComparator idComparator = new IdComparator<>(); + private final IdComparator idComparator = new IdComparator<>(); private static final String DEFAULT_FILE_NAME = "test.jks"; private static final String DEFAULT_FILE_NAME_2 = "test2.jks"; @@ -126,13 +126,10 @@ public class TbResourceControllerTest extends AbstractControllerTest { Assert.assertEquals(DEFAULT_FILE_NAME, savedResource.getResourceKey()); Assert.assertArrayEquals(resource.getData(), download(savedResource.getId())); - TbResource foundResource = doGet("/api/resource/" + savedResource.getId().getId().toString(), TbResource.class); - foundResource.setTitle("My new resource"); - foundResource.setData(null); - - savedResource = save(foundResource); - - Assert.assertEquals(foundResource.getTitle(), savedResource.getTitle()); + String resourceTitle = "My new resource"; + savedResource.setTitle(resourceTitle); + savedResource = doPut("/api/resource/" + savedResource.getUuidId() + "/info", savedResource, TbResourceInfo.class); + assertThat(savedResource.getTitle()).isEqualTo(resourceTitle); testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(savedResource, savedResource.getId(), savedResource.getId(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), @@ -501,8 +498,8 @@ public class TbResourceControllerTest extends AbstractControllerTest { savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, cntEntity, cntEntity, cntEntity); - Collections.sort(resources, idComparator); - Collections.sort(loadedResources, idComparator); + resources.sort(idComparator); + loadedResources.sort(idComparator); Assert.assertEquals(resources, loadedResources); } @@ -549,8 +546,8 @@ public class TbResourceControllerTest extends AbstractControllerTest { savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, jksCntEntity + lwm2mCntEntity, jksCntEntity + lwm2mCntEntity, jksCntEntity + lwm2mCntEntity); - Collections.sort(resources, idComparator); - Collections.sort(loadedResources, idComparator); + resources.sort(idComparator); + loadedResources.sort(idComparator); Assert.assertEquals(resources, loadedResources); } @@ -581,8 +578,8 @@ public class TbResourceControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(resources, idComparator); - Collections.sort(loadedResources, idComparator); + resources.sort(idComparator); + loadedResources.sort(idComparator); Assert.assertEquals(resources, loadedResources); @@ -654,8 +651,8 @@ public class TbResourceControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(jksResources, idComparator); - Collections.sort(loadedResources, idComparator); + jksResources.sort(idComparator); + loadedResources.sort(idComparator); Assert.assertEquals(jksResources, loadedResources); @@ -736,8 +733,8 @@ public class TbResourceControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Collections.sort(expectedResources, idComparator); - Collections.sort(loadedResources, idComparator); + expectedResources.sort(idComparator); + loadedResources.sort(idComparator); Assert.assertEquals(expectedResources, loadedResources); @@ -770,7 +767,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { MockHttpServletResponse response = resultActions.andReturn().getResponse(); String eTag = response.getHeader("ETag"); Assert.assertNotNull(eTag); - Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA); + Assert.assertEquals(TEST_DATA, Base64.getEncoder().encodeToString(response.getContentAsByteArray())); //download with if-none-match header HttpHeaders headers = new HttpHeaders(); @@ -814,7 +811,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { MockHttpServletResponse response = resultActions.andReturn().getResponse(); String eTag = response.getHeader("ETag"); Assert.assertNotNull(eTag); - Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA); + Assert.assertEquals(TEST_DATA, Base64.getEncoder().encodeToString(response.getContentAsByteArray())); //download with if-none-match header HttpHeaders headers = new HttpHeaders(); @@ -859,10 +856,10 @@ public class TbResourceControllerTest extends AbstractControllerTest { .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString("can't be updated"))); - foundResource.setData(null); - foundResource.setTitle("Updated resource"); - savedResource = doPost("/api/resource", foundResource, TbResource.class); - assertThat(savedResource.getTitle()).isEqualTo("Updated resource"); + String resourceTitle = "Updated resource"; + savedResource.setTitle(resourceTitle); + savedResource = doPut("/api/resource/" + savedResource.getUuidId() + "/info", savedResource, TbResourceInfo.class); + assertThat(savedResource.getTitle()).isEqualTo(resourceTitle); assertThat(savedResource.getFileName()).isEqualTo(resource.getFileName()); assertThat(savedResource.getEtag()).isEqualTo(resource.getEtag()); assertThat(download(savedResource.getId())).asBase64Encoded().isEqualTo(TEST_DATA); @@ -923,8 +920,20 @@ public class TbResourceControllerTest extends AbstractControllerTest { } private TbResourceInfo save(TbResource tbResource) throws Exception { - return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>() { - }); + byte[] data = tbResource.getData() != null ? tbResource.getData() : tbResource.getEncodedData() != null ? Base64.getDecoder().decode(tbResource.getEncodedData()) : null; + List parts = new ArrayList<>(); + parts.add(new MockPart("resourceType", tbResource.getResourceType().name().getBytes())); + if (tbResource.getTitle() != null) { + parts.add(new MockPart("title", tbResource.getTitle().getBytes())); + } + if (tbResource.getDescriptor() != null) { + parts.add(new MockPart("descriptor", tbResource.getDescriptor().toString().getBytes())); + } + if (tbResource.getResourceSubType() != null) { + parts.add(new MockPart("resourceSubType", tbResource.getResourceSubType().name().getBytes())); + } + + return uploadResource(HttpMethod.POST, "/api/resource/upload", tbResource.getFileName(), tbResource.getResourceType().getMediaType(), data, parts); } private TbResourceInfo findResourceInfo(TbResourceId id) throws Exception { @@ -949,7 +958,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { for (String model : models) { String fileName = model + ".xml"; - byte[] bytes = IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream("lwm2m/" + fileName)); + byte[] bytes = IOUtils.toByteArray(Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream("lwm2m/" + fileName))); TbResource resource = new TbResource(); resource.setResourceType(ResourceType.LWM2M_MODEL); diff --git a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java index bebdd5ecb9..6dacf50592 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java @@ -34,6 +34,7 @@ import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -58,6 +59,7 @@ import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.subscription.SubscriptionErrorCode; import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; @@ -478,6 +480,56 @@ public class WebsocketApiTest extends AbstractControllerTest { Assert.assertFalse(alarmStatusUpdate5.isActive()); } + @Test + public void testAlarmStatusWsCmdForPropagatedAlarms() throws Exception { + loginTenantAdmin(); + Device device = new Device(); + device.setName("Test device"); + device.setLabel("Label"); + device.setType("default"); + device = doPost("/api/device", device, Device.class); + + Asset asset = new Asset(); + asset.setName("My asset"); + asset.setType("default"); + asset = doPost("/api/asset", asset, Asset.class); + + EntityRelation entityRelation = new EntityRelation(asset.getId(), device.getId(), "CONTAINS"); + doPost("/api/relation", entityRelation); + + AlarmStatusCmd cmd = new AlarmStatusCmd(1, asset.getId(), null, List.of(AlarmSeverity.CRITICAL)); + getWsClient().send(cmd); + + AlarmStatusUpdate update = JacksonUtil.fromString(getWsClient().waitForReply(), AlarmStatusUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + Assert.assertFalse(update.isActive()); + + //create alarm + getWsClient().registerWaitForUpdate(); + + Alarm alarm = Alarm.builder() + .originator(device.getId()) + .severity(AlarmSeverity.CRITICAL) + .type("test_type") + .propagate(true) + .build(); + + alarm = doPost("/api/alarm", alarm, Alarm.class); + Assert.assertNotNull(alarm); + + // check no update for asset + String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + Assert.assertNull(msg); + + // check device + AlarmStatusCmd deviceCmd = new AlarmStatusCmd(2, device.getId(), null, List.of(AlarmSeverity.CRITICAL)); + getWsClient().send(deviceCmd); + + AlarmStatusUpdate deviceUpdate = JacksonUtil.fromString(getWsClient().waitForReply(), AlarmStatusUpdate.class); + Assert.assertEquals(2, deviceUpdate.getCmdId()); + Assert.assertTrue(deviceUpdate.isActive()); + } + @Test public void testAlarmStatusWsCmdWithMaxAlarmsCacheSize() throws Exception { loginTenantAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java index 8150456efb..1c2aca46ec 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java @@ -27,11 +27,15 @@ import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; @@ -183,6 +187,22 @@ public class DashboardEdgeTest extends AbstractEdgeTest { customer.setTitle("Edge Customer"); Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + // assign edge to customer + edgeImitator.expectMessageAmount(2); + doPost("/api/customer/" + savedCustomer.getUuidId() + "/edge/" + edge.getUuidId(), Edge.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + Optional 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 customerUpdateOpt = edgeImitator.findMessageByType(CustomerUpdateMsg.class); + Assert.assertTrue(customerUpdateOpt.isPresent()); + CustomerUpdateMsg customerUpdateMsg = customerUpdateOpt.get(); + Customer customerMsg = JacksonUtil.fromString(customerUpdateMsg.getEntity(), Customer.class, true); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); + Assert.assertEquals(savedCustomer, customerMsg); + Dashboard dashboard = buildDashboardForUplinkMsg(savedCustomer); // create dashboard on edge @@ -225,6 +245,23 @@ public class DashboardEdgeTest extends AbstractEdgeTest { foundDashboard = doGet("/api/dashboard/" + dashboard.getUuidId(), Dashboard.class); Assert.assertEquals(DASHBOARD_TITLE + " Updated", foundDashboard.getName()); + + // unassign edge from customer + edgeImitator.expectMessageAmount(2); + doDelete("/api/customer/edge/" + edge.getUuidId(), Edge.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + edgeConfigurationOpt = edgeImitator.findMessageByType(EdgeConfiguration.class); + Assert.assertTrue(edgeConfigurationOpt.isPresent()); + edgeConfiguration = edgeConfigurationOpt.get(); + Assert.assertEquals( + new CustomerId(EntityId.NULL_UUID), + new CustomerId(new UUID(edgeConfiguration.getCustomerIdMSB(), edgeConfiguration.getCustomerIdLSB()))); + customerUpdateOpt = edgeImitator.findMessageByType(CustomerUpdateMsg.class); + Assert.assertTrue(customerUpdateOpt.isPresent()); + customerUpdateMsg = customerUpdateOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); + Assert.assertEquals(savedCustomer.getUuidId().getMostSignificantBits(), customerUpdateMsg.getIdMSB()); + Assert.assertEquals(savedCustomer.getUuidId().getLeastSignificantBits(), customerUpdateMsg.getIdLSB()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java index 2f244807bd..610e27944e 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java @@ -19,8 +19,8 @@ import com.google.common.collect.Lists; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.CustomerId; @@ -61,7 +61,7 @@ public class EdqsEntityServiceTest extends EntityServiceTest { @Autowired private EdqsService edqsService; - @MockBean + @MockitoBean private EdqsRocksDb edqsRocksDb; @Before diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index c6ad3c12d3..cc55e09fd5 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.google.common.collect.Streams; +import org.apache.commons.lang3.tuple.Pair; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -45,10 +46,15 @@ import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; @@ -306,6 +312,54 @@ public class VersionControlTest extends AbstractControllerTest { checkImportedOtaPackageData(software, importedSoftwareOta); } + @Test + public void testDeviceVc_withAlarmRules_betweenTenants() throws Exception { + DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile of tenant 1"); + Dashboard dashboard = createDashboard(null, "Mobile dashboard"); + createAlarmRule(deviceProfile.getId(), "Profile alarm rule", dashboard.getId()); + Device device = createDevice(deviceProfile.getId(), "Device of tenant 1", "test1"); + createAlarmRule(device.getId(), "Device alarm rule", dashboard.getId()); + String version = createVersion("devices, profiles and dashboards", EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.DASHBOARD); + + loginTenant2(); + Map result = loadVersion(version, config -> { + config.setLoadCredentials(false); + }, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.DASHBOARD); + assertThat(result.get(EntityType.DEVICE).getCreated()).isEqualTo(1); + assertThat(result.get(EntityType.DEVICE_PROFILE).getCreated()).isEqualTo(1); + assertThat(result.get(EntityType.DASHBOARD).getCreated()).isEqualTo(1); + + Device importedDevice = findDevice(device.getName()); + checkImportedEntity(tenantId1, device, tenantId2, importedDevice); + checkImportedDeviceData(device, importedDevice); + + DeviceProfile importedDeviceProfile = findDeviceProfile(deviceProfile.getName()); + checkImportedEntity(tenantId1, deviceProfile, tenantId2, importedDeviceProfile); + checkImportedDeviceProfileData(deviceProfile, importedDeviceProfile); + assertThat(importedDevice.getDeviceProfileId()).isEqualTo(importedDeviceProfile.getId()); + + Dashboard importedDashboard = findDashboard(dashboard.getName()); + checkImportedEntity(tenantId1, dashboard, tenantId2, importedDashboard); + checkImportedDashboardData(dashboard, importedDashboard); + + getCalculatedFields(CalculatedFieldType.ALARM, EntityType.DEVICE_PROFILE, + List.of(importedDeviceProfile.getUuidId()), null).forEach(alarmRuleCf -> { + assertThat(alarmRuleCf.getName()).isEqualTo("Profile alarm rule"); + AlarmCalculatedFieldConfiguration config = (AlarmCalculatedFieldConfiguration) alarmRuleCf.getConfiguration(); + config.getAllRules().map(Pair::getValue).forEach(alarmRule -> { + assertThat(alarmRule.getDashboardId()).isEqualTo(importedDashboard.getId()); + }); + }); + getCalculatedFields(CalculatedFieldType.ALARM, EntityType.DEVICE_PROFILE, + List.of(importedDevice.getUuidId()), null).forEach(alarmRuleCf -> { + assertThat(alarmRuleCf.getName()).isEqualTo("Device alarm rule"); + AlarmCalculatedFieldConfiguration config = (AlarmCalculatedFieldConfiguration) alarmRuleCf.getConfiguration(); + config.getAllRules().map(Pair::getValue).forEach(alarmRule -> { + assertThat(alarmRule.getDashboardId()).isEqualTo(importedDashboard.getId()); + }); + }); + } + @Test public void testDashboardVc_betweenTenants() throws Exception { Dashboard dashboard = createDashboard(null, "Dashboard of tenant 1"); @@ -368,42 +422,42 @@ public class VersionControlTest extends AbstractControllerTest { String aliasId = "23c4185d-1497-9457-30b2-6d91e69a5b2c"; String unknownUuid = "ea0dc8b0-3d85-11ed-9200-77fc04fa14fa"; String entityAliases = "{\n" + - "\"" + aliasId + "\": {\n" + - "\"alias\": \"assets\",\n" + - "\"filter\": {\n" + - " \"entityList\": [\n" + - " \"" + asset1.getId() + "\",\n" + - " \"" + asset2.getId() + "\",\n" + - " \"" + tenantId1.getId() + "\",\n" + - " \"" + existingDeviceProfile.getId() + "\",\n" + - " \"" + unknownUuid + "\"\n" + - " ],\n" + - " \"id\":\"" + asset1.getId() + "\",\n" + - " \"resolveMultiple\": true\n" + - "},\n" + - "\"id\": \"" + aliasId + "\"\n" + - "}\n" + - "}"; + "\"" + aliasId + "\": {\n" + + "\"alias\": \"assets\",\n" + + "\"filter\": {\n" + + " \"entityList\": [\n" + + " \"" + asset1.getId() + "\",\n" + + " \"" + asset2.getId() + "\",\n" + + " \"" + tenantId1.getId() + "\",\n" + + " \"" + existingDeviceProfile.getId() + "\",\n" + + " \"" + unknownUuid + "\"\n" + + " ],\n" + + " \"id\":\"" + asset1.getId() + "\",\n" + + " \"resolveMultiple\": true\n" + + "},\n" + + "\"id\": \"" + aliasId + "\"\n" + + "}\n" + + "}"; String widgetId = "ea8f34a0-264a-f11f-cde3-05201bb4ff4b"; String actionId = "4a8e6efa-3e68-fa59-7feb-d83366130cae"; String widgets = "{\n" + - " \"" + widgetId + "\": {\n" + - " \"config\": {\n" + - " \"actions\": {\n" + - " \"rowClick\": [\n" + - " {\n" + - " \"name\": \"go to dashboard\",\n" + - " \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" + - " \"id\": \"" + actionId + "\"\n" + - " }\n" + - " ]\n" + - " }\n" + - " },\n" + - " \"row\": 0,\n" + - " \"col\": 0,\n" + - " \"id\": \"" + widgetId + "\"\n" + - " }\n" + - "}"; + " \"" + widgetId + "\": {\n" + + " \"config\": {\n" + + " \"actions\": {\n" + + " \"rowClick\": [\n" + + " {\n" + + " \"name\": \"go to dashboard\",\n" + + " \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" + + " \"id\": \"" + actionId + "\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"row\": 0,\n" + + " \"col\": 0,\n" + + " \"id\": \"" + widgetId + "\"\n" + + " }\n" + + "}"; ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode(); dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases)); @@ -499,11 +553,12 @@ public class VersionControlTest extends AbstractControllerTest { generatorNodeConfig.setMsgCount(1); generatorNodeConfig.setScriptLang(ScriptLanguage.JS); UUID someUuid = UUID.randomUUID(); - generatorNodeConfig.setJsScript("var msg = { temp: 42, humidity: 77 };\n" + - "var metadata = { data: 40 };\n" + - "var msgType = \"POST_TELEMETRY_REQUEST\";\n" + - "var someUuid = \"" + someUuid + "\";\n" + - "return { msg: msg, metadata: metadata, msgType: msgType };"); + generatorNodeConfig.setJsScript(""" + var msg = { temp: 42, humidity: 77 }; + var metadata = { data: 40 }; + var msgType = "POST_TELEMETRY_REQUEST"; + var someUuid = "%s"; + return { msg: msg, metadata: metadata, msgType: msgType };""".formatted(someUuid)); generatorNode.setConfiguration(JacksonUtil.valueToTree(generatorNodeConfig)); nodes.add(generatorNode); metaData.setNodes(nodes); @@ -1018,20 +1073,21 @@ public class VersionControlTest extends AbstractControllerTest { protected Dashboard createDashboard(CustomerId customerId, String name, AssetId assetForEntityAlias) { Dashboard dashboard = createDashboard(customerId, name); - String entityAliases = "{\n" + - "\t\"23c4185d-1497-9457-30b2-6d91e69a5b2c\": {\n" + - "\t\t\"alias\": \"assets\",\n" + - "\t\t\"filter\": {\n" + - "\t\t\t\"entityList\": [\n" + - "\t\t\t\t\"" + assetForEntityAlias.getId().toString() + "\"\n" + - "\t\t\t],\n" + - "\t\t\t\"entityType\": \"ASSET\",\n" + - "\t\t\t\"resolveMultiple\": true,\n" + - "\t\t\t\"type\": \"entityList\"\n" + - "\t\t},\n" + - "\t\t\"id\": \"23c4185d-1497-9457-30b2-6d91e69a5b2c\"\n" + - "\t}\n" + - "}"; + String entityAliases = """ + { + "23c4185d-1497-9457-30b2-6d91e69a5b2c": { + "alias": "assets", + "filter": { + "entityList": [ + "%s" + ], + "entityType": "ASSET", + "resolveMultiple": true, + "type": "entityList" + }, + "id": "23c4185d-1497-9457-30b2-6d91e69a5b2c" + } + }""".formatted(assetForEntityAlias.getId().toString()); ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode(); dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases)); dashboardConfiguration.set("description", new TextNode("hallo")); @@ -1134,6 +1190,33 @@ public class VersionControlTest extends AbstractControllerTest { return doPost("/api/calculatedField", calculatedField, CalculatedField.class); } + private CalculatedField createAlarmRule(EntityId entityId, String alarmType, DashboardId mobileDashboardId) { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setName(alarmType); + calculatedField.setType(CalculatedFieldType.ALARM); + AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); + configuration.setArguments(arguments); + SimpleAlarmCondition createCondition = new SimpleAlarmCondition(); + createCondition.setExpression(new TbelAlarmConditionExpression("return temperature >= 50;")); + configuration.setCreateRules(Map.of( + AlarmSeverity.CRITICAL, new AlarmRule(createCondition, "", mobileDashboardId) + )); + SimpleAlarmCondition clearCondition = new SimpleAlarmCondition(); + clearCondition.setExpression(new TbelAlarmConditionExpression("return temperature < 50;")); + configuration.setClearRule(new AlarmRule(clearCondition, "", mobileDashboardId)); + calculatedField.setConfiguration(configuration); + calculatedField.setDebugSettings(DebugSettings.all()); + return saveCalculatedField(calculatedField); + } + private CalculatedFieldConfiguration getCalculatedFieldConfig(EntityId referencedEntityId) { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index 153228a865..3c62333122 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -162,7 +162,7 @@ class DefaultTelemetrySubscriptionServiceTest { apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); lenient().when(apiUsageStateService.getApiUsageState(tenantId)).thenReturn(apiUsageState); - lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId)).thenReturn(tpi); + lenient().when(partitionService.resolve(eq(ServiceType.TB_CORE), eq(tenantId), any())).thenReturn(tpi); lenient().when(tsService.save(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); lenient().when(tsService.saveWithoutLatest(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), null))); @@ -310,8 +310,6 @@ class DefaultTelemetrySubscriptionServiceTest { given(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).willReturn(immediateFuture(List.of(entityView))); // mock that save latest call for entity view is successful given(tsService.saveLatest(tenantId, entityView.getId(), sampleTimeseries)).willReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); - // mock TPI for entity view - given(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).willReturn(tpi); var request = TimeseriesSaveRequest.builder() .tenantId(tenantId) @@ -373,8 +371,6 @@ class DefaultTelemetrySubscriptionServiceTest { lenient().when(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).thenReturn(immediateFuture(List.of(entityView))); // mock that save latest call for entity view is successful lenient().when(tsService.saveLatest(tenantId, entityView.getId(), sampleTimeseries)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); - // mock TPI for entity view - lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).thenReturn(tpi); var request = TimeseriesSaveRequest.builder() .tenantId(tenantId) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java index 09a363853f..ddefa966f0 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java @@ -70,11 +70,10 @@ public class TbCaffeineCacheConfiguration { } private CaffeineCache buildCache(String name, CacheSpecs cacheSpec) { - - final Caffeine caffeineBuilder - = Caffeine.newBuilder() + Caffeine caffeineBuilder = Caffeine.newBuilder() .weigher(collectionSafeWeigher()) .maximumWeight(cacheSpec.getMaxSize()) + .recordStats() .ticker(ticker()); if (!cacheSpec.getTimeToLiveInMinutes().equals(0)) { caffeineBuilder.expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java index 0d5d3dcd13..2cdad499de 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java @@ -48,7 +48,7 @@ public interface AttributesService { List findAllKeysByEntityIds(TenantId tenantId, List entityIds); - List findAllKeysByEntityIds(TenantId tenantId, List entityIds, String scope); + List findAllKeysByEntityIds(TenantId tenantId, List entityIds, AttributeScope scope); int removeAllByEntityId(TenantId tenantId, EntityId entityId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java index e239e22ee9..0b88ce17cc 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java @@ -63,5 +63,8 @@ public interface TimeseriesService { List findAllKeysByEntityIds(TenantId tenantId, List entityIds); + ListenableFuture> findAllKeysByEntityIdsAsync(TenantId tenantId, List entityIds); + void cleanup(long systemTtl); + } diff --git a/common/data/pom.xml b/common/data/pom.xml index 779565ed8e..3afd7cf779 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -113,7 +113,7 @@ compile - dev.langchain4j + org.thingsboard.langchain4j langchain4j-core diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java index a3bf383503..77de18e446 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import java.io.Serial; import java.util.function.UnaryOperator; @Schema @@ -36,6 +37,7 @@ import java.util.function.UnaryOperator; @EqualsAndHashCode(callSuper = true) public class TbResourceInfo extends BaseData implements HasName, HasTenantId, ExportableEntity { + @Serial private static final long serialVersionUID = 7282664529021651736L; @Schema(description = "JSON object with Tenant Id. Tenant Id of the resource can't be changed.", accessMode = Schema.AccessMode.READ_ONLY) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java index 9a4e875154..7dcf7ffe66 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java @@ -18,11 +18,15 @@ package org.thingsboard.server.common.data.alarm.rule; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition; import org.thingsboard.server.common.data.id.DashboardId; @Data +@AllArgsConstructor +@NoArgsConstructor public class AlarmRule { @Valid @@ -33,7 +37,7 @@ public class AlarmRule { @JsonIgnore public boolean requiresScheduledReevaluation() { - return condition.hasSchedule(); + return condition.requiresScheduledReevaluation(); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java index 9bb549994b..073d9347fa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java @@ -21,9 +21,10 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.NoArgsConstructor; -import org.jetbrains.annotations.NotNull; import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule; import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AnyTimeSchedule; @@ -50,6 +51,20 @@ public abstract class AlarmCondition { return schedule != null && !(schedule.getStaticValue() instanceof AnyTimeSchedule); } + @JsonIgnore + public boolean requiresScheduledReevaluation() { + return hasSchedule() || expression.requiresScheduledReevaluation(); + } + + @JsonIgnore + @AssertTrue(message = "Expressions requiring scheduled reevaluation can only be used with simple alarm conditions") + public boolean isValid() { + if (getType() != AlarmConditionType.SIMPLE && expression.requiresScheduledReevaluation()) { + return false; + } + return true; + } + @JsonIgnore public abstract AlarmConditionType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java index e855f8efd3..0502a10105 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java @@ -32,4 +32,9 @@ public interface AlarmConditionExpression { @JsonIgnore AlarmConditionExpressionType getType(); + @JsonIgnore + default boolean requiresScheduledReevaluation() { + return false; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java index e99849ea82..4c6df3825d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java @@ -17,8 +17,11 @@ package org.thingsboard.server.common.data.alarm.rule.condition.expression; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.ComplexFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.FilterPredicateType; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate; import org.thingsboard.server.common.data.query.EntityKeyValueType; @@ -34,7 +37,21 @@ public class AlarmConditionFilter implements Serializable { private EntityKeyValueType valueType; private ComplexOperation operation; @Valid - @NotNull + @NotEmpty private List predicates; + public boolean hasPredicate(FilterPredicateType type) { + return containsPredicate(predicates, type); + } + + private boolean containsPredicate(List predicates, FilterPredicateType type) { + return predicates.stream().anyMatch(predicate -> { + if (predicate instanceof ComplexFilterPredicate complexPredicate) { + return containsPredicate(complexPredicate.getPredicates(), type); + } else { + return predicate.getType() == type; + } + }); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java index 8c27400961..b0afbcc7ba 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java @@ -20,6 +20,7 @@ import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.FilterPredicateType; import java.util.List; @@ -38,4 +39,9 @@ public class SimpleAlarmConditionExpression implements AlarmConditionExpression return AlarmConditionExpressionType.SIMPLE; } + @Override + public boolean requiresScheduledReevaluation() { + return filters.stream().anyMatch(filter -> filter.hasPredicate(FilterPredicateType.NO_DATA)); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java index 50f73e887b..2562e19be4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java @@ -16,9 +16,13 @@ package org.thingsboard.server.common.data.alarm.rule.condition.expression; import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@AllArgsConstructor +@NoArgsConstructor public class TbelAlarmConditionExpression implements AlarmConditionExpression { @NotBlank diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java index af7c45ac5b..687b39932e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java @@ -19,5 +19,6 @@ public enum FilterPredicateType { STRING, NUMERIC, BOOLEAN, + NO_DATA, COMPLEX } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java index 58355c627d..ca4531c123 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java @@ -27,7 +27,9 @@ import java.io.Serializable; @Type(value = StringFilterPredicate.class, name = "STRING"), @Type(value = NumericFilterPredicate.class, name = "NUMERIC"), @Type(value = BooleanFilterPredicate.class, name = "BOOLEAN"), - @Type(value = ComplexFilterPredicate.class, name = "COMPLEX")}) + @Type(value = NoDataFilterPredicate.class, name = "NO_DATA"), + @Type(value = ComplexFilterPredicate.class, name = "COMPLEX") +}) public interface KeyFilterPredicate extends Serializable { @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java new file mode 100644 index 0000000000..82b366f6ae --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java @@ -0,0 +1,43 @@ +/** + * 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.common.data.alarm.rule.condition.expression.predicate; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; + +import java.util.concurrent.TimeUnit; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class NoDataFilterPredicate implements KeyFilterPredicate { + + @NotNull + private TimeUnit unit; + @Valid + @NotNull + private AlarmConditionValue duration; + + @Override + public FilterPredicateType getType() { + return FilterPredicateType.NO_DATA; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java index 65316eda88..4bc547695b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java @@ -17,10 +17,14 @@ package org.thingsboard.server.common.data.alarm.rule.condition.expression.predi import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; @Data +@AllArgsConstructor +@NoArgsConstructor public class NumericFilterPredicate implements SimpleKeyFilterPredicate { @NotNull diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java index d36ba33849..6f3cdbdd3c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java @@ -87,4 +87,11 @@ public class AlarmCalculatedFieldConfiguration implements ArgumentsBasedCalculat }); } + public boolean propagationSettingsEqual(AlarmCalculatedFieldConfiguration other) { + return this.propagate == other.propagate && + this.propagateToOwner == other.propagateToOwner && + this.propagateToTenant == other.propagateToTenant && + Objects.equals(this.propagateRelationTypes, other.propagateRelationTypes); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeys.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeys.java new file mode 100644 index 0000000000..4cac646908 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeys.java @@ -0,0 +1,67 @@ +/** + * 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.common.data.query; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.EntityType; + +import java.util.List; +import java.util.Set; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static java.util.Objects.requireNonNullElse; + +@Schema( + description = "Contains unique time series and attribute key names discovered from entities matching a query. Used primarily for UI hints such as autocomplete suggestions." +) +public record AvailableEntityKeys( + @Schema( + description = "Set of entity types found among the matched entities.", + example = "[\"DEVICE\", \"ASSET\"]", + requiredMode = Schema.RequiredMode.REQUIRED + ) + Set entityTypes, + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + @ArraySchema( + arraySchema = @Schema(description = "List of unique time series key names available on the matched entities."), + schema = @Schema(implementation = String.class, example = "temperature"), + uniqueItems = true + ) + List timeseries, + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + @ArraySchema( + arraySchema = @Schema(description = "List of unique attribute key names available on the matched entities."), + schema = @Schema(implementation = String.class, example = "serialNumber"), + uniqueItems = true + ) + List attribute +) { + + public AvailableEntityKeys { + entityTypes = requireNonNullElse(entityTypes, emptySet()); + timeseries = requireNonNullElse(timeseries, emptyList()); + attribute = requireNonNullElse(attribute, emptyList()); + } + + public static AvailableEntityKeys none() { + return new AvailableEntityKeys(emptySet(), emptyList(), emptyList()); + } + +} diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index dbaef23431..b2f47b4b73 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -136,12 +136,25 @@ public class EdgeGrpcClient implements EdgeRpcClient { .setConnectRequestMsg(ConnectRequestMsg.newBuilder() .setEdgeRoutingKey(edgeKey) .setEdgeSecret(edgeSecret) - .setEdgeVersion(EdgeVersion.V_4_3_0) + .setEdgeVersion(getNewestEdgeVersion()) .setMaxInboundMessageSize(maxInboundMessageSize) .build()) .build()); } + public static EdgeVersion getNewestEdgeVersion() { + EdgeVersion newest = null; + for (EdgeVersion v : EdgeVersion.values()) { + if (v == EdgeVersion.V_LATEST || v == EdgeVersion.UNRECOGNIZED) { + continue; + } + if (newest == null || v.getNumber() > newest.getNumber()) { + newest = v; + } + } + return newest; + } + private StreamObserver initOutputStream(String edgeKey, Consumer onUplinkResponse, Consumer onEdgeUpdate, diff --git a/common/queue/pom.xml b/common/queue/pom.xml index ffee41f8a9..df592338ef 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -68,6 +68,10 @@ org.apache.kafka kafka-clients + + org.lz4 + lz4-java + com.google.cloud google-cloud-pubsub diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index 6542492238..a2ad982765 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -105,6 +105,7 @@ public abstract class AbstractGatewaySessionHandler { ack(msg, MqttReasonCodes.PubAck.SUCCESS); log.trace("[{}][{}][{}] onDeviceConnectOk: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName); }, - t -> logDeviceCreationError(t, deviceName)); + t -> processFailure(msgId, deviceName, CONNECT, ackSent, t)); } public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java index 5527d17add..a1af985887 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java @@ -53,7 +53,7 @@ public interface AttributesDao { List findAllKeysByEntityIds(TenantId tenantId, List entityIds); - List findAllKeysByEntityIdsAndAttributeType(TenantId tenantId, List entityIds, String attributeType); + List findAllKeysByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope); List> removeAllByEntityId(TenantId tenantId, EntityId entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 62b35caa7f..362aa95ee6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -28,7 +28,6 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ObjectType; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edqs.AttributeKv; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; @@ -93,11 +92,11 @@ public class BaseAttributesService implements AttributesService { } @Override - public List findAllKeysByEntityIds(TenantId tenantId, List entityIds, String scope) { - if (StringUtils.isEmpty(scope)) { + public List findAllKeysByEntityIds(TenantId tenantId, List entityIds, AttributeScope scope) { + if (scope == null) { return attributesDao.findAllKeysByEntityIds(tenantId, entityIds); } else { - return attributesDao.findAllKeysByEntityIdsAndAttributeType(tenantId, entityIds, scope); + return attributesDao.findAllKeysByEntityIdsAndScope(tenantId, entityIds, scope); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index 44b2daaf8e..11ba48773d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -212,11 +212,11 @@ public class CachedAttributesService implements AttributesService { } @Override - public List findAllKeysByEntityIds(TenantId tenantId, List entityIds, String scope) { - if (StringUtils.isEmpty(scope)) { + public List findAllKeysByEntityIds(TenantId tenantId, List entityIds, AttributeScope scope) { + if (scope == null) { return attributesDao.findAllKeysByEntityIds(tenantId, entityIds); } else { - return attributesDao.findAllKeysByEntityIdsAndAttributeType(tenantId, entityIds, scope); + return attributesDao.findAllKeysByEntityIdsAndScope(tenantId, entityIds, scope); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index 1eed9a6b59..2177d493e3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -91,7 +91,9 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @Primary public class BaseResourceService extends AbstractCachedEntityService implements ResourceService { - public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId "; + protected static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId "; + protected static final int MAX_ENTITIES_TO_FIND = 10; + protected final TbResourceDao resourceDao; protected final TbResourceInfoDao resourceInfoDao; protected final ResourceDataValidator resourceValidator; @@ -100,7 +102,6 @@ public class BaseResourceService extends AbstractCachedEntityService> resourceLinkContainerDaoMap = new HashMap<>(); private final Map> generalResourceContainerDaoMap = new HashMap<>(); - protected static final int MAX_ENTITIES_TO_FIND = 10; @PostConstruct public void init() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java index f9f03adeff..fa035c4a80 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java @@ -17,9 +17,11 @@ package org.thingsboard.server.dao.service.validator; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; @@ -54,8 +56,8 @@ public class ResourceDataValidator extends DataValidator { @Override protected TbResource validateUpdate(TenantId tenantId, TbResource resource) { - if (resource.getData() != null && !resource.getResourceType().isUpdatable() && - tenantId != null && !tenantId.isSysTenantId()) { + if ((resource.getData() != null && !resource.getResourceType().isUpdatable() && tenantId != null && !tenantId.isSysTenantId()) + || resource.getResourceType().equals(ResourceType.LWM2M_MODEL)) { throw new DataValidationException("This type of resource can't be updated"); } return resource; @@ -81,7 +83,7 @@ public class ResourceDataValidator extends DataValidator { if (StringUtils.isEmpty(resource.getFileName())) { throw new DataValidationException("Resource file name should be specified!"); } - if (StringUtils.containsAny(resource.getFileName(), "/", "\\")) { + if (Strings.CS.containsAny(resource.getFileName(), "/", "\\")) { throw new DataValidationException("File name contains forbidden symbols"); } if (StringUtils.isEmpty(resource.getResourceKey())) { @@ -104,4 +106,5 @@ public class ResourceDataValidator extends DataValidator { validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, dataSize, TB_RESOURCE); } } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index 0a8b8f6399..1e58b23a1f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -177,10 +177,12 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl } @Override - public List findAllKeysByEntityIdsAndAttributeType(TenantId tenantId, List entityIds, String attributeType) { + public List findAllKeysByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope) { return attributeKvRepository - .findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).collect(Collectors.toList()), AttributeScope.valueOf(attributeType).getId()) - .stream().map(id -> keyDictionaryDao.getKey(id)).collect(Collectors.toList()); + .findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).toList(), scope.getId()) + .stream() + .map(keyDictionaryDao::getKey) + .toList(); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java index d45182442a..bac5529249 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java @@ -167,5 +167,9 @@ public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseries return sqlDao.findAllKeysByEntityIds(tenantId, entityIds); } + @Override + public ListenableFuture> findAllKeysByEntityIdsAsync(TenantId tenantId, List entityIds) { + return sqlDao.findAllKeysByEntityIdsAsync(tenantId, entityIds); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java index c546fc21ea..27470bfe8a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java @@ -24,7 +24,6 @@ import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.Page; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; @@ -38,8 +37,6 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.dictionary.KeyDictionaryDao; @@ -64,7 +61,6 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.function.Function; -import java.util.stream.Collectors; @Slf4j @Component @@ -185,9 +181,13 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme @Override public List findAllKeysByEntityIds(TenantId tenantId, List entityIds) { - return tsKvLatestRepository.findAllKeysByEntityIds(entityIds.stream().map(EntityId::getId).collect(Collectors.toList())); + return tsKvLatestRepository.findAllKeysByEntityIds(entityIds.stream().map(EntityId::getId).toList()); } + @Override + public ListenableFuture> findAllKeysByEntityIdsAsync(TenantId tenantId, List entityIds) { + return service.submit(() -> findAllKeysByEntityIds(tenantId, entityIds)); + } private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query, Long version) { ListenableFuture> future = findNewLatestEntryFuture(tenantId, entityId, query); @@ -211,7 +211,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme ReadTsKvQueryResult::getData, MoreExecutors.directExecutor()); } - protected TsKvEntry doFindLatestSync(EntityId entityId, String key) { + protected TsKvEntry doFindLatestSync(EntityId entityId, String key) { TsKvLatestCompositeKey compositeKey = new TsKvLatestCompositeKey( entityId.getId(), diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index ceb7fcf822..de197bf88f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -156,6 +156,11 @@ public class BaseTimeseriesService implements TimeseriesService { return timeseriesLatestDao.findAllKeysByEntityIds(tenantId, entityIds); } + @Override + public ListenableFuture> findAllKeysByEntityIdsAsync(TenantId tenantId, List entityIds) { + return timeseriesLatestDao.findAllKeysByEntityIdsAsync(tenantId, entityIds); + } + @Override public void cleanup(long systemTtl) { timeseriesDao.cleanup(systemTtl); @@ -300,13 +305,13 @@ public class BaseTimeseriesService implements TimeseriesService { long interval = query.getInterval(); if (interval < 1) { throw new IncorrectParameterException("Invalid TsKvQuery: 'interval' must be greater than 0, but got " + interval + - ". Please check your query parameters and ensure 'endTs' is greater than 'startTs' or increase 'interval'."); + ". Please check your query parameters and ensure 'endTs' is greater than 'startTs' or increase 'interval'."); } long step = Math.max(interval, 1000); long intervalCounts = (query.getEndTs() - query.getStartTs()) / step; if (intervalCounts > maxTsIntervals || intervalCounts < 0) { throw new IncorrectParameterException("Incorrect TsKvQuery. Number of intervals is to high - " + intervalCounts + ". " + - "Please increase 'interval' parameter for your query or reduce the time range of the query."); + "Please increase 'interval' parameter for your query or reduce the time range of the query."); } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java index 54a7e68725..dd44a62349 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java @@ -36,17 +36,13 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import org.thingsboard.server.dao.nosql.TbResultSet; import org.thingsboard.server.dao.sqlts.AggregationTimeseriesDao; import org.thingsboard.server.dao.util.NoSqlTsLatestDao; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Optional; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; @@ -103,6 +99,10 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes return Collections.emptyList(); } + @Override + public ListenableFuture> findAllKeysByEntityIdsAsync(TenantId tenantId, List entityIds) { + return Futures.immediateFuture(Collections.emptyList()); + } @Override public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java index 32479301ae..74c041e4d3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java @@ -22,12 +22,8 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import java.util.List; -import java.util.Map; import java.util.Optional; public interface TimeseriesLatestDao { @@ -54,4 +50,6 @@ public interface TimeseriesLatestDao { List findAllKeysByEntityIds(TenantId tenantId, List entityIds); + ListenableFuture> findAllKeysByEntityIdsAsync(TenantId tenantId, List entityIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java index cae2d56ae5..46a7b650f7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java @@ -117,24 +117,26 @@ public class DeviceConnectivityUtil { dockerComposeBuilder.append("\n"); dockerComposeBuilder.append(" # Environment variables\n"); dockerComposeBuilder.append(" environment:\n"); - dockerComposeBuilder.append(" - host=").append(isLocalhost(host) ? HOST_DOCKER_INTERNAL : host).append("\n"); - dockerComposeBuilder.append(" - port=1883\n"); + dockerComposeBuilder.append(" - TB_GW_HOST=").append(isLocalhost(host) ? HOST_DOCKER_INTERNAL : host).append("\n"); + dockerComposeBuilder.append(" - TB_GW_PORT=1883\n"); switch (deviceCredentials.getCredentialsType()) { case ACCESS_TOKEN: - dockerComposeBuilder.append(" - accessToken=").append(deviceCredentials.getCredentialsId()).append("\n"); + dockerComposeBuilder.append(" - TB_GW_SECURITY_TYPE=accessToken\n"); + dockerComposeBuilder.append(" - TB_GW_ACCESS_TOKEN=").append(deviceCredentials.getCredentialsId()).append("\n"); break; case MQTT_BASIC: + dockerComposeBuilder.append(" - TB_GW_SECURITY_TYPE=usernamePassword\n"); BasicMqttCredentials credentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), BasicMqttCredentials.class); if (credentials != null) { if (StringUtils.isNotEmpty(credentials.getClientId())) { - dockerComposeBuilder.append(" - clientId=").append(credentials.getClientId()).append("\n"); + dockerComposeBuilder.append(" - TB_GW_CLIENT_ID=").append(credentials.getClientId()).append("\n"); } if (StringUtils.isNotEmpty(credentials.getUserName())) { - dockerComposeBuilder.append(" - username=").append(credentials.getUserName()).append("\n"); + dockerComposeBuilder.append(" - TB_GW_USERNAME=").append(credentials.getUserName()).append("\n"); } if (StringUtils.isNotEmpty(credentials.getPassword())) { - dockerComposeBuilder.append(" - password=").append(credentials.getPassword()).append("\n"); + dockerComposeBuilder.append(" - TB_GW_PASSWORD=").append(credentials.getPassword()).append("\n"); } } break; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java index 5978d903c7..41a013e4c9 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java @@ -223,7 +223,7 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123"); Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { - List keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId), AttributeScope.SERVER_SCOPE.name()); + List keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId), AttributeScope.SERVER_SCOPE); assertThat(keys).containsOnly("key1", "key2"); }); } diff --git a/pom.xml b/pom.xml index b0768966bd..38cb432a02 100755 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,7 @@ 5.6.0 3.9.3 3.25.5 - 1.68.1 + 1.76.0 1.2.8 1.18.38 1.2.5 @@ -86,6 +86,7 @@ org.apache.kafka.common.network.NetworkReceive class in the application module. It addresses the issue https://issues.apache.org/jira/browse/KAFKA-4090. Here is the source to track https://github.com/apache/kafka/tree/trunk/clients/src/main/java/org/apache/kafka/common/network --> 3.9.1 + 1.8.1 8.10.1 3.5.3 2.2 @@ -110,7 +111,7 @@ 4.0.2 1.7.5 3.8.0 - 1.1.0 + 1.8.0-TB 2.38.0 1.24 1.11.0 @@ -911,7 +912,7 @@ import - dev.langchain4j + org.thingsboard.langchain4j langchain4j-bom ${langchain4j.version} pom @@ -1153,6 +1154,17 @@ org.apache.kafka kafka-clients ${kafka.version} + + + org.lz4 + lz4-java + + + + + org.lz4 + lz4-java + ${lz4.version} com.github.springtestdbunit @@ -1908,6 +1920,10 @@ central https://repo1.maven.org/maven2/ + + thingsboard-repo + https://repo.thingsboard.io/artifactory/libs-release-public + spring-snapshots Spring Snapshots diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 2c8e01d246..6eb61b3e13 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -39,10 +39,12 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.rest.client.utils.RestJsonConverter; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.ClaimRequest; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; @@ -160,6 +162,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.AvailableEntityKeys; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -592,7 +595,7 @@ public class RestClient implements Closeable { } public PageData getAllAlarmsV2(List statusList, List severityList, - List typeList, String assignedId, TimePageLink pageLink) { + List typeList, String assignedId, TimePageLink pageLink) { String urlSecondPart = "/api/v2/alarms?"; Map params = new HashMap<>(); if (!CollectionUtils.isEmpty(statusList)) { @@ -1824,12 +1827,15 @@ public class RestClient implements Closeable { }).getBody(); } - public JsonNode findEntityTimeseriesAndAttributesKeysByQuery(EntityDataQuery query) { - return restTemplate.exchange( - baseURL + "/api/entitiesQuery/find/keys", - HttpMethod.POST, new HttpEntity<>(query), - new ParameterizedTypeReference() { - }).getBody(); + public AvailableEntityKeys findAvailableEntityKeysByQuery(EntityDataQuery query, boolean includeTimeseries, boolean includeAttributes, AttributeScope scope) { + var uri = UriComponentsBuilder.fromUriString(baseURL) + .path("/api/entitiesQuery/find/keys") + .queryParam("timeseries", includeTimeseries) + .queryParam("attributes", includeAttributes) + .queryParamIfPresent("scope", Optional.ofNullable(scope)) + .build() + .toUri(); + return restTemplate.exchange(uri, HttpMethod.POST, new HttpEntity<>(query), new ParameterizedTypeReference() {}).getBody(); } public PageData findAlarmDataByQuery(AlarmDataQuery query) { diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 59397d4a92..0c1bb2e362 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -99,7 +99,7 @@ provided - dev.langchain4j + org.thingsboard.langchain4j langchain4j diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 98c7fde665..954a6c7ef1 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -96,6 +96,10 @@ org.apache.kafka kafka-clients + + org.lz4 + lz4-java + com.amazonaws aws-java-sdk-sns @@ -154,7 +158,7 @@ json-path - dev.langchain4j + org.thingsboard.langchain4j langchain4j diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 580b63a588..3403e7a3aa 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -141,6 +141,9 @@ "rollup": "4.22.4", "@babel/core": "7.25.2", "esbuild": "0.23.0", - "jquery.terminal/coveralls-next/form-data": "4.0.4" + "jquery.terminal/coveralls-next/form-data": "4.0.4", + "node-forge": "1.3.3", + "glob": "10.5.0", + "path-to-regexp": "0.1.12" } } diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 8c4120739e..f27a86c2ac 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -192,6 +192,7 @@ export interface IStateController { getStateIdAtIndex(index: number): string; getEntityId(entityParamName: string): EntityId; getCurrentStateName(): string; + reInit(): void; } export interface SubscriptionInfo { diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 168c63b3b1..81c20be472 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -17,7 +17,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; -import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { defaultHttpOptionsFromConfig, defaultHttpUploadOptions, RequestConfig } from '@core/http/http-utils'; import { forkJoin, Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; import { Resource, ResourceInfo, ResourceSubType, ResourceType, TBResourceScope } from '@shared/models/resource.models'; @@ -90,6 +90,54 @@ export class ResourceService { return this.http.post('/api/resource', resource, defaultHttpOptionsFromConfig(config)); } + public uploadResources(resources: Resource[], config?: RequestConfig): Observable { + let partSize = 100; + partSize = resources.length > partSize ? partSize : resources.length; + const resourceObservables: Observable[] = []; + for (let i = 0; i < partSize; i++) { + resourceObservables.push(this.uploadResource(resources[i], config).pipe(catchError(() => of({} as Resource)))); + } + return forkJoin(resourceObservables).pipe( + mergeMap((resource) => { + resources.splice(0, partSize); + if (resources.length) { + return this.uploadResources(resources, config); + } else { + return of(resource); + } + }) + ); + } + + public uploadResource(resource: Resource, config?: RequestConfig): Observable { + if (!config) { + config = {}; + } + const formData = new FormData(); + formData.append('file', resource.data); + formData.append('title', resource.title); + formData.append('resourceType', resource.resourceType); + if (resource.resourceSubType) { + formData.append('resourceSubType', resource.resourceSubType); + } + return this.http.post('/api/resource/upload', formData, + defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest)); + } + + public updatedResourceInfo(resourceId: string, updatedResources: Partial>, config?: RequestConfig): Observable { + return this.http.put(`/api/resource/${resourceId}/info`, updatedResources, defaultHttpOptionsFromConfig(config)); + } + + public updatedResourceData(resourceId: string, data: File, config?: RequestConfig): Observable { + if (!config) { + config = {}; + } + const formData = new FormData(); + formData.append('file', data); + return this.http.put(`/api/resource/${resourceId}/data`, formData, + defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest)); + } + public deleteResource(resourceId: string, force = false, config?: RequestConfig) { return this.http.delete(`/api/resource/${resourceId}?force=${force}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 8909663771..555717ffc0 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -510,7 +510,7 @@ export const menuSectionMap = new Map([ MenuId.alarms, { id: MenuId.alarms, - name: 'alarm.alarms', + name: 'alarm.alarm-list', type: 'link', path: '/alarms/alarms', icon: 'mdi:alert-outline' diff --git a/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts index dd2c27cb47..cd593b4291 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts @@ -56,9 +56,7 @@ export class CheckConnectivityDialogComponent extends DialogComponent - +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html index b734757144..20bd3e1bde 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -130,30 +130,32 @@ {{ 'alarm-rule.advanced-settings' | translate }} -
- - {{ 'alarm-rule.propagate-alarm' | translate }} - +
+
+ + {{ 'alarm-rule.propagate-alarm' | translate }} + +
+ @if (configFormGroup.get('propagate').value) { + + alarm-rule.alarm-rule-relation-types-list + + + {{key}} + close + + + + + + }
- @if (configFormGroup.get('propagate').value) { - - alarm-rule.alarm-rule-relation-types-list - - - {{key}} - close - - - - - - }
{{ 'alarm-rule.propagate-alarm-to-owner' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html index a71874c13c..5deddffd87 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html @@ -66,7 +66,7 @@
-
alarm-rule.entity-type
+
alarm-rule.target-entity-type
{{ 'alarm-rule.any-type' | translate }} @@ -78,7 +78,7 @@
@if (alarmRuleFilterConfigForm.get('entityType').value) {
-
alarm-rule.alarm-rule-entity-list
+
alarm-rule.target-entities
{ this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', this.pageMode ? '30%' :'33%', entity => this.utilsService.customTranslation(entity.name, entity.name))); if (this.pageMode) { - this.columns.push(new EntityTableColumn('entityType', 'alarm-rule.entity-type', '15%', + this.columns.push(new EntityTableColumn('entityType', 'alarm-rule.target-entity-type', '15%', entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); - this.columns.push(new EntityLinkTableColumn('entityName', 'alarm-rule.entity-name', '30%', + this.columns.push(new EntityLinkTableColumn('entityName', 'alarm-rule.target-entity', '30%', entity => this.utilsService.customTranslation(entity['entityName'], entity['entityName']), entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false)); } this.columns.push(new EntityTableColumn('createRule', 'alarm-rule.severities', this.pageMode ? '15%' :'67%', entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '), () => ({}), false)); - this.columns.push(new EntityTableColumn('clearRule', 'alarm-rule.cleared', '70px', + this.columns.push(new EntityTableColumn('clearRule', 'alarm-rule.cleared', '90px', entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: 0, textAlign: 'center'}}, false)); this.cellActionDescriptors.push( diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html index 34f165420a..d67cd51a9a 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html @@ -90,15 +90,47 @@
}
+ + @if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) { +
+ + + {{ 'alarm-rule.filter-preview' | translate }} + + +
+ @if (specText) { + {{ specText }} + } + @if (conditionFormGroup.get('expression.filters').value?.length) { + + + } @else { + {{ 'alarm-rule.no-filter-preview' | translate }} + } +
+
+
+
+ } +
{{ 'alarm-rule.condition-settings' | translate }}
alarm-rule.condition-type - + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} + @if (isNoData) { + alarm-rule.condition-type-hint + } @if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) {
@@ -146,7 +178,7 @@
- +
@@ -162,9 +194,8 @@ @if (!readonly) { } @@ -186,6 +217,9 @@ } @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('pattern')) { {{ defaultValuePatternError | translate }} } + @if (type === AlarmConditionType.REPEATING) { + alarm-rule.condition-repeating-value-hint + } @@ -198,9 +232,14 @@ {{ argument }} } - - {{ 'calculated-fields.hint.argument-name-required' | translate }} - + @if (conditionFormGroup.get(groupName).get('dynamicValueArgument').hasError('required')) { + + {{ 'calculated-fields.hint.argument-name-required' | translate }} + + } + @if (type === AlarmConditionType.REPEATING) { + alarm-rule.condition-repeating-value-hint + } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts index 2b0f25c2fe..f565a66168 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts @@ -24,13 +24,6 @@ import { DialogComponent } from '@app/shared/components/dialog.component'; import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ScriptLanguage } from "@shared/models/rule-node.models"; -import { - AlarmRuleCondition, - AlarmRuleConditionType, - AlarmRuleConditionTypeTranslationMap, - alarmRuleDefaultScript, - AlarmRuleExpressionType -} from "@shared/models/alarm-rule.models"; import { CalculatedFieldArgument, getCalculatedFieldArgumentsEditorCompleter, @@ -38,8 +31,18 @@ import { } from "@shared/models/calculated-field.models"; import { TbEditorCompleter } from "@shared/models/ace/completion.models"; import { AceHighlightRules } from "@shared/models/ace/ace.models"; -import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models"; +import { ComplexOperation } from "@shared/models/query/query.models"; import { Observable } from "rxjs"; +import { TranslateService } from "@ngx-translate/core"; +import { + AlarmRuleCondition, + AlarmRuleConditionType, + AlarmRuleConditionTypeTranslationMap, + alarmRuleDefaultScript, + AlarmRuleExpressionType, + AlarmRuleFilter, + filterOperationTranslationMap +} from "@shared/models/alarm-rule.models"; export interface CfAlarmRuleConditionDialogData { readonly: boolean; @@ -96,7 +99,11 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent(false); ComplexOperation = ComplexOperation; - complexOperationTranslationMap = complexOperationTranslationMap; + complexOperationTranslationMap = filterOperationTranslationMap; + + specText = ''; + + filtersValid: boolean = false; functionArgs: Array; argumentsEditorCompleter: TbEditorCompleter; @@ -105,11 +112,14 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent; + isNoData: boolean = false; + constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData, public dialogRef: MatDialogRef, - private fb: FormBuilder) { + private fb: FormBuilder, + private translate: TranslateService) { super(store, router, dialogRef); this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)]; @@ -128,21 +138,38 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent { - this.updateValidators(type, true); + this.updateValidators(type); + }); + + this.conditionFormGroup.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value) => { + this.updateSpecText(value.type); + }) + + this.conditionFormGroup.get('expression.filters').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((filters) => { + this.filtersValid = this.areFilterAndPredicateArgumentsValid(filters, this.argumentsList); + this.checkIsNoData(filters); }); this.conditionFormGroup.get('expression.type').valueChanges.pipe( @@ -164,6 +191,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent) { + this.isNoData = this.hasNoData(filters); + if (this.isNoData && this.conditionFormGroup.get('type').value !== AlarmRuleConditionType.SIMPLE) { + this.conditionFormGroup.get('type').patchValue(AlarmRuleConditionType.SIMPLE); + } + } + + private hasNoData(data: Array) { + const search = (filter) => { + if (!filter) return false; + if (Array.isArray(filter)) return filter.some(search); + if (typeof filter !== 'object') return false; + if (filter.type === 'NO_DATA') return true; + if (filter.predicates?.length) return filter.predicates.some(search); + return false; + }; + return search(data); + }; + + private updateValidators(type: AlarmRuleConditionType) { switch (type) { case AlarmRuleConditionType.DURATION: this.conditionFormGroup.get('unit').enable({emitEvent: false}); @@ -218,6 +298,55 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent 0) { + this.specText = this.specText + ':'; + } + } + cancel(): void { this.dialogRef.close(null); } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html index 0b722b4ec1..638075849f 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html @@ -21,7 +21,7 @@
{{ 'alarm-rule.schedule-title' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts index 543b0a86dc..6f17aa8b59 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts @@ -14,8 +14,9 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; +import { ChangeDetectorRef, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; import { + AbstractControl, ControlValueAccessor, FormBuilder, NG_VALIDATORS, @@ -68,7 +69,7 @@ import { Observable } from "rxjs"; } ] }) -export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator { +export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator, OnChanges { @Input() @coerceBoolean() @@ -96,11 +97,15 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali specText = ''; + filtersArgumentsValid: boolean = true; + schedulerArgumentsValid: boolean = true; + scheduleText = ''; private modelValue: AlarmRuleCondition; private propagateChange = (v: any) => { }; + private onValidatorChange = () => { }; constructor(private dialog: MatDialog, private fb: FormBuilder, @@ -115,6 +120,10 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali registerOnTouched(fn: any): void { } + registerOnValidatorChange(fn: () => void): void { + this.onValidatorChange = fn; + } + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { @@ -127,14 +136,68 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali writeValue(value: AlarmRuleCondition): void { this.modelValue = value; this.updateConditionInfo(); + if (value) { + this.onValidatorChange(); + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.arguments) { + if (changes.arguments && !changes.arguments.firstChange && this.modelValue) { + this.onValidatorChange(); + } + } + } + + private isScheduleArgumentValid(obj: any, validArguments: string[]): boolean { + const arg = obj?.schedule?.dynamicValueArgument; + return !arg || validArguments.includes(arg); + } + + private areFilterAndPredicateArgumentsValid(obj: any, validArguments: string[]): boolean { + const validSet = new Set(validArguments); + const filters = obj?.expression?.filters || obj?.filters || []; + for (const filter of filters) { + if (filter.argument && !validSet.has(filter.argument)) { + return false; + } + } + function checkPredicates(predicates: any[]): boolean { + for (const p of predicates) { + if (p.value?.dynamicValueArgument) { + if (!validSet.has(p.value.dynamicValueArgument)) { + return false; + } + } + if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) { + if (!checkPredicates(p.predicates)) { + return false; + } + } + } + return true; + } + for (const filter of filters) { + if (Array.isArray(filter.predicates)) { + if (!checkPredicates(filter.predicates)) { + return false; + } + } + } + return true; } public conditionSet() { return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters); } - public validate(): ValidationErrors | null { - return this.conditionSet() ? null : { + public validate(control: AbstractControl): ValidationErrors | null { + this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, Object.keys(this.arguments)); + this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments)); + this.onValidatorChange = () => { + control.updateValueAndValidity({ emitEvent: true }); + }; + return this.conditionSet() && this.filtersArgumentsValid && this.schedulerArgumentsValid ? null : { alarmRuleCondition: { valid: false, } @@ -226,6 +289,9 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali private updateModel() { this.updateConditionInfo(); this.propagateChange(this.modelValue); + if (this.modelValue) { + this.onValidatorChange(); + } } public openScheduleDialog($event: Event) { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html index 38e0bd8023..a9f4ad61a5 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html @@ -20,11 +20,11 @@ @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
-
+
alarm-rule.alarm-rule-additional-info
- +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss index b66573f417..6253aa9a4b 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss @@ -20,6 +20,11 @@ display: grid; grid-template-rows: min-content minmax(auto, 1fr) min-content; } + + .spec-text { + font-size: 14px; + color: rgba(0, 0, 0, 0.54); + } } .tbel-script-lang-chip { line-height: 20px; diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts index b1c7a4da7f..a4572629e0 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts @@ -171,7 +171,7 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator if (value) { this.modelValue = value; if (this.modelValue.dynamicValueArgument) { - this.alarmScheduleForm.get('dynamicValueArgument').patchValue(this.modelValue.dynamicValueArgument, {emitEvent: false}); + this.alarmScheduleForm.get('dynamicValueArgument').patchValue(Object.keys(this.arguments).includes(this.modelValue.dynamicValueArgument) ? this.modelValue.dynamicValueArgument : null, {emitEvent: false}); } else { switch (this.modelValue.staticValue.type) { case AlarmRuleScheduleType.SPECIFIC_TIME: diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html index 75bce37a80..c0c5a72d77 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html @@ -25,20 +25,25 @@
- - filter.operation.operation - - - {{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}} - - - - - +
+
+
+ {{ 'alarm-rule.filters' | translate }} +
+ + {{ complexOperationTranslations.get(complexOperationEnum.AND) | translate }} + {{ complexOperationTranslations.get(complexOperationEnum.OR) | translate }} + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts index 6961524bdd..3f86bdbe04 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts @@ -21,18 +21,18 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; - import { - ComplexOperation, - complexOperationTranslationMap, - EntityKeyValueType, - entityKeyValueTypesMap - } from '@shared/models/query/query.models'; + import { ComplexOperation, EntityKeyValueType, entityKeyValueTypesMap } from '@shared/models/query/query.models'; import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; - import { AlarmRuleFilter, AlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; + import { + AlarmRuleFilter, + AlarmRuleFilterPredicate, + filterOperationTranslationMap + } from "@shared/models/alarm-rule.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { FormControlsFrom } from "@shared/models/tenant.model"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; + import { isDefinedAndNotNull } from "@core/utils"; export interface AlarmRuleFilterDialogData { filter: AlarmRuleFilter; @@ -57,7 +57,9 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent { + this.predicatesValid = this.isPredicateArgumentsValid(predicates); + }); + this.filterFormGroup.get('valueType').valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe((valueType: EntityKeyValueType) => { @@ -106,6 +116,31 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent
-
{{ filterControl.value?.argument }}
-
{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.predicates[0]?.type) | translate }}
-
+
{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}
+ @@ -62,14 +69,15 @@ }
} - - filter.no-key-filters - + @if (!filtersFormArray.length) { + + alarm-rule.no-filter + + + }
- -