139 changed files with 5153 additions and 543 deletions
@ -0,0 +1,28 @@ |
|||
/** |
|||
* Copyright © 2016-2021 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.id; |
|||
|
|||
import org.junit.Assert; |
|||
import org.junit.Test; |
|||
|
|||
public class EntityIdTest { |
|||
|
|||
@Test |
|||
public void givenConstantNullUuid_whenCompare_thenToStringEqualsPredefinedUuid() { |
|||
Assert.assertEquals("13814000-1dd2-11b2-8080-808080808080", EntityId.NULL_UUID.toString()); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.dao; |
|||
|
|||
import org.junit.extensions.cpsuite.ClasspathSuite; |
|||
import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters; |
|||
import org.junit.runner.RunWith; |
|||
|
|||
@RunWith(ClasspathSuite.class) |
|||
@ClassnameFilters({ |
|||
"org.thingsboard.server.dao.service.psql.*SqlTest", |
|||
"org.thingsboard.server.dao.service.attributes.psql.*SqlTest", |
|||
"org.thingsboard.server.dao.service.event.psql.*SqlTest", |
|||
"org.thingsboard.server.dao.service.timeseries.psql.*SqlTest" |
|||
}) |
|||
public class PostgreSqlDaoServiceTestSuite { |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.dao; |
|||
|
|||
import com.google.common.base.Charsets; |
|||
import com.google.common.io.Resources; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
import java.io.IOException; |
|||
import java.net.URL; |
|||
import java.sql.Connection; |
|||
import java.sql.SQLException; |
|||
import java.util.List; |
|||
|
|||
@Slf4j |
|||
public class PostgreSqlInitializer { |
|||
|
|||
private static final List<String> sqlFiles = List.of( |
|||
"sql/schema-ts-psql.sql", |
|||
"sql/schema-entities.sql", |
|||
"sql/schema-entities-idx.sql", |
|||
"sql/system-data.sql", |
|||
"sql/system-test-psql.sql"); |
|||
private static final String dropAllTablesSqlFile = "sql/psql/drop-all-tables.sql"; |
|||
|
|||
public static void initDb(Connection conn) { |
|||
cleanUpDb(conn); |
|||
log.info("initialize Postgres DB..."); |
|||
try { |
|||
for (String sqlFile : sqlFiles) { |
|||
URL sqlFileUrl = Resources.getResource(sqlFile); |
|||
String sql = Resources.toString(sqlFileUrl, Charsets.UTF_8); |
|||
conn.createStatement().execute(sql); |
|||
} |
|||
} catch (IOException | SQLException e) { |
|||
throw new RuntimeException("Unable to init the Postgres database. Reason: " + e.getMessage(), e); |
|||
} |
|||
log.info("Postgres DB is initialized!"); |
|||
} |
|||
|
|||
private static void cleanUpDb(Connection conn) { |
|||
log.info("clean up Postgres DB..."); |
|||
try { |
|||
URL dropAllTableSqlFileUrl = Resources.getResource(dropAllTablesSqlFile); |
|||
String dropAllTablesSql = Resources.toString(dropAllTableSqlFileUrl, Charsets.UTF_8); |
|||
conn.createStatement().execute(dropAllTablesSql); |
|||
} catch (IOException | SQLException e) { |
|||
throw new RuntimeException("Unable to clean up the Postgres database. Reason: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.dao.service; |
|||
|
|||
import org.springframework.test.context.TestPropertySource; |
|||
|
|||
import java.lang.annotation.Documented; |
|||
import java.lang.annotation.ElementType; |
|||
import java.lang.annotation.Inherited; |
|||
import java.lang.annotation.Retention; |
|||
import java.lang.annotation.RetentionPolicy; |
|||
import java.lang.annotation.Target; |
|||
|
|||
@Target(ElementType.TYPE) |
|||
@Retention(RetentionPolicy.RUNTIME) |
|||
@Inherited |
|||
@Documented |
|||
@TestPropertySource(locations = {"classpath:application-test.properties", "classpath:psql-test.properties"}) |
|||
public @interface DaoPostgreSqlTest { |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.dao.sql.query; |
|||
|
|||
import org.junit.Test; |
|||
import org.junit.runner.RunWith; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
import org.springframework.boot.test.mock.mockito.MockBean; |
|||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; |
|||
import org.springframework.test.context.junit4.SpringRunner; |
|||
import org.springframework.transaction.support.TransactionTemplate; |
|||
|
|||
import static org.hamcrest.MatcherAssert.assertThat; |
|||
import static org.hamcrest.Matchers.equalTo; |
|||
|
|||
@RunWith(SpringRunner.class) |
|||
@SpringBootTest(classes = DefaultEntityQueryRepository.class) |
|||
public class DefaultEntityQueryRepositoryTest { |
|||
|
|||
@MockBean |
|||
NamedParameterJdbcTemplate jdbcTemplate; |
|||
@MockBean |
|||
TransactionTemplate transactionTemplate; |
|||
@MockBean |
|||
DefaultQueryLogComponent queryLog; |
|||
|
|||
@Autowired |
|||
DefaultEntityQueryRepository repo; |
|||
|
|||
/* |
|||
* This value has to be reasonable small to prevent infinite recursion as early as possible |
|||
* */ |
|||
@Test |
|||
public void givenDefaultMaxLevel_whenStaticConstant_thenEqualsTo() { |
|||
assertThat(repo.getMaxLevelAllowed(), equalTo(50)); |
|||
} |
|||
|
|||
@Test |
|||
public void givenMaxLevelZeroOrNegative_whenGetMaxLevel_thenReturnDefaultMaxLevel() { |
|||
assertThat(repo.getMaxLevel(0), equalTo(repo.getMaxLevelAllowed())); |
|||
assertThat(repo.getMaxLevel(-1), equalTo(repo.getMaxLevelAllowed())); |
|||
assertThat(repo.getMaxLevel(-2), equalTo(repo.getMaxLevelAllowed())); |
|||
assertThat(repo.getMaxLevel(Integer.MIN_VALUE), equalTo(repo.getMaxLevelAllowed())); |
|||
} |
|||
|
|||
@Test |
|||
public void givenMaxLevelPositive_whenGetMaxLevel_thenValueTheSame() { |
|||
assertThat(repo.getMaxLevel(1), equalTo(1)); |
|||
assertThat(repo.getMaxLevel(2), equalTo(2)); |
|||
assertThat(repo.getMaxLevel(repo.getMaxLevelAllowed()), equalTo(repo.getMaxLevelAllowed())); |
|||
assertThat(repo.getMaxLevel(repo.getMaxLevelAllowed() + 1), equalTo(repo.getMaxLevelAllowed())); |
|||
assertThat(repo.getMaxLevel(Integer.MAX_VALUE), equalTo(repo.getMaxLevelAllowed())); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
database.ts.type=sql |
|||
database.ts_latest.type=sql |
|||
sql.ts_inserts_executor_type=fixed |
|||
sql.ts_inserts_fixed_thread_pool_size=200 |
|||
sql.ts_key_value_partitioning=MONTHS |
|||
# |
|||
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true |
|||
spring.jpa.properties.hibernate.order_by.default_null_ordering=last |
|||
spring.jpa.properties.hibernate.jdbc.log.warnings=false |
|||
spring.jpa.show-sql=false |
|||
spring.jpa.hibernate.ddl-auto=none |
|||
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect |
|||
spring.datasource.username=postgres |
|||
spring.datasource.password=postgres |
|||
spring.datasource.url=jdbc:tc:postgresql:12.8:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb |
|||
spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver |
|||
#org.postgresql.Driver |
|||
spring.datasource.hikari.maximumPoolSize=50 |
|||
service.type=monolith |
|||
#database.ts.type=timescale |
|||
#database.ts.type=sql |
|||
#database.entities.type=sql |
|||
# |
|||
#sql.ts_inserts_executor_type=fixed |
|||
#sql.ts_inserts_fixed_thread_pool_size=200 |
|||
#sql.ts_key_value_partitioning=MONTHS |
|||
# |
|||
#spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true |
|||
#spring.jpa.show-sql=false |
|||
#spring.jpa.hibernate.ddl-auto=none |
|||
#spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect |
|||
# |
|||
#spring.datasource.username=postgres |
|||
#spring.datasource.password=postgres |
|||
#spring.datasource.url=jdbc:postgresql://localhost:5432/sqltest |
|||
#spring.datasource.driverClassName=org.postgresql.Driver |
|||
#spring.datasource.hikari.maximumPoolSize = 50 |
|||
queue.core.pack-processing-timeout=3000 |
|||
queue.rule-engine.pack-processing-timeout=3000 |
|||
queue.rule-engine.queues[0].name=Main |
|||
queue.rule-engine.queues[0].topic=tb_rule_engine.main |
|||
queue.rule-engine.queues[0].poll-interval=25 |
|||
queue.rule-engine.queues[0].partitions=3 |
|||
queue.rule-engine.queues[0].pack-processing-timeout=3000 |
|||
queue.rule-engine.queues[0].processing-strategy.type=SKIP_ALL_FAILURES |
|||
queue.rule-engine.queues[0].submit-strategy.type=BURST |
|||
sql.log_entity_queries=true |
|||
@ -0,0 +1,2 @@ |
|||
--PostgreSQL specific truncate to fit constraints |
|||
TRUNCATE TABLE device_credentials, device, device_profile, rule_node_state, rule_node, rule_chain; |
|||
@ -0,0 +1,68 @@ |
|||
#### Clear alarm details builder function |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
*function Details(msg, metadata, msgType): any* |
|||
|
|||
JavaScript function generating **Alarm Details** object to update existing one. Used for storing additional parameters inside Alarm.<br> |
|||
For example you can save attribute name/value pair from Original Message payload or Metadata. |
|||
|
|||
**Parameters:** |
|||
|
|||
{% include rulenode/common_node_script_args %} |
|||
|
|||
**Returns:** |
|||
|
|||
Should return the object presenting **Alarm Details**. |
|||
|
|||
Current Alarm Details can be accessed via `metadata.prevAlarmDetails`.<br> |
|||
**Note** that `metadata.prevAlarmDetails` is a raw String field, and it needs to be converted into object using this construction: |
|||
|
|||
```javascript |
|||
var details = {}; |
|||
if (metadata.prevAlarmDetails) { |
|||
// remove prevAlarmDetails from metadata |
|||
delete metadata.prevAlarmDetails; |
|||
details = JSON.parse(metadata.prevAlarmDetails); |
|||
} |
|||
{:copy-code} |
|||
``` |
|||
|
|||
<div class="divider"></div> |
|||
|
|||
##### Examples |
|||
|
|||
<ul> |
|||
<li> |
|||
Take <code>count</code> property from previous Alarm and increment it.<br> |
|||
Also put <code>temperature</code> attribute from inbound Message payload into Alarm details: |
|||
</li> |
|||
</ul> |
|||
|
|||
```javascript |
|||
var details = {temperature: msg.temperature, count: 1}; |
|||
|
|||
if (metadata.prevAlarmDetails) { |
|||
var prevDetails = JSON.parse(metadata.prevAlarmDetails); |
|||
// remove prevAlarmDetails from metadata |
|||
delete metadata.prevAlarmDetails; |
|||
if (prevDetails.count) { |
|||
details.count = prevDetails.count + 1; |
|||
} |
|||
} |
|||
|
|||
return details; |
|||
{:copy-code} |
|||
``` |
|||
|
|||
<br> |
|||
|
|||
More details about Alarms can be found in [this tutorial{:target="_blank"}](${baseUrl}/docs/user-guide/alarms/). |
|||
|
|||
You can see the real life example, where this node is used, in the next tutorial: |
|||
|
|||
- [Create and Clear Alarms{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/) |
|||
|
|||
<br> |
|||
<br> |
|||
@ -0,0 +1,8 @@ |
|||
<ul> |
|||
<li><b>msg:</b> <code>{[key: string]: any}</code> - is a Message payload key/value object. |
|||
</li> |
|||
<li><b>metadata:</b> <code>{[key: string]: string}</code> - is a Message metadata key/value object. |
|||
</li> |
|||
<li><b>msgType:</b> <code>string</code> - is a string Message type. See <a href="https://github.com/thingsboard/thingsboard/blob/ea039008b148453dfa166cf92bc40b26e487e660/ui-ngx/src/app/shared/models/rule-node.models.ts#L338" target="_blank">MessageType</a> enum for common used values. |
|||
</li> |
|||
</ul> |
|||
@ -0,0 +1,69 @@ |
|||
#### Create alarm details builder function |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
*function Details(msg, metadata, msgType): any* |
|||
|
|||
JavaScript function generating **Alarm Details** object. Used for storing additional parameters inside Alarm.<br> |
|||
For example you can save attribute name/value pair from Original Message payload or Metadata. |
|||
|
|||
**Parameters:** |
|||
|
|||
{% include rulenode/common_node_script_args %} |
|||
|
|||
**Returns:** |
|||
|
|||
Should return the object presenting **Alarm Details**. |
|||
|
|||
**Optional:** previous Alarm Details can be accessed via `metadata.prevAlarmDetails`.<br> |
|||
If previous Alarm does not exist, this field will not be present in Metadata. **Note** that `metadata.prevAlarmDetails`<br> |
|||
is a raw String field, and it needs to be converted into object using this construction: |
|||
|
|||
```javascript |
|||
var details = {}; |
|||
if (metadata.prevAlarmDetails) { |
|||
// remove prevAlarmDetails from metadata |
|||
delete metadata.prevAlarmDetails; |
|||
details = JSON.parse(metadata.prevAlarmDetails); |
|||
} |
|||
{:copy-code} |
|||
``` |
|||
|
|||
<div class="divider"></div> |
|||
|
|||
##### Examples |
|||
|
|||
<ul> |
|||
<li> |
|||
Take <code>count</code> property from previous Alarm and increment it.<br> |
|||
Also put <code>temperature</code> attribute from inbound Message payload into Alarm details: |
|||
</li> |
|||
</ul> |
|||
|
|||
```javascript |
|||
var details = {temperature: msg.temperature, count: 1}; |
|||
|
|||
if (metadata.prevAlarmDetails) { |
|||
var prevDetails = JSON.parse(metadata.prevAlarmDetails); |
|||
// remove prevAlarmDetails from metadata |
|||
delete metadata.prevAlarmDetails; |
|||
if (prevDetails.count) { |
|||
details.count = prevDetails.count + 1; |
|||
} |
|||
} |
|||
|
|||
return details; |
|||
{:copy-code} |
|||
``` |
|||
|
|||
<br> |
|||
|
|||
More details about Alarms can be found in [this tutorial{:target="_blank"}](${baseUrl}/docs/user-guide/alarms/). |
|||
|
|||
You can see the real life example, where this node is used, in the next tutorial: |
|||
|
|||
- [Create and Clear Alarms{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/) |
|||
|
|||
<br> |
|||
<br> |
|||
@ -0,0 +1,69 @@ |
|||
#### Filter message function |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
*function Filter(msg, metadata, msgType): boolean* |
|||
|
|||
JavaScript function evaluating **true/false** condition on incoming Message. |
|||
|
|||
**Parameters:** |
|||
|
|||
{% include rulenode/common_node_script_args %} |
|||
|
|||
**Returns:** |
|||
|
|||
Should return `boolean` value. If `true` - send Message via **True** chain, otherwise **False** chain is used. |
|||
|
|||
<div class="divider"></div> |
|||
|
|||
##### Examples |
|||
|
|||
* Forward all messages with `temperature` value greater than `20` to the **True** chain and all other messages to the **False** chain: |
|||
|
|||
```javascript |
|||
return msg.temperature > 20; |
|||
{:copy-code} |
|||
``` |
|||
|
|||
* Forward all messages with type `ATTRIBUTES_UPDATED` to the **True** chain and all other messages to the **False** chain: |
|||
|
|||
```javascript |
|||
if (msgType === 'ATTRIBUTES_UPDATED') { |
|||
return true; |
|||
} else { |
|||
return false; |
|||
} |
|||
{:copy-code} |
|||
``` |
|||
|
|||
<ul> |
|||
<li>Send message to the <strong>True</strong> chain if the following conditions are met.<br>Message type is <code>POST_TELEMETRY_REQUEST</code> and<br> |
|||
(device type is <code>vehicle</code> and <code>humidity</code> value is greater than <code>50</code> or<br> |
|||
device type is <code>controller</code> and <code>temperature</code> value is greater than <code>20</code> and <code>humidity</code> value is greater than <code>60</code>).<br> |
|||
Otherwise send message to the <strong>False</strong> chain: |
|||
</li> |
|||
</ul> |
|||
|
|||
```javascript |
|||
if (msgType === 'POST_TELEMETRY_REQUEST') { |
|||
if (metadata.deviceType === 'vehicle') { |
|||
return msg.humidity > 50; |
|||
} else if (metadata.deviceType === 'controller') { |
|||
return msg.temperature > 20 && msg.humidity > 60; |
|||
} |
|||
} |
|||
return false; |
|||
{:copy-code} |
|||
``` |
|||
|
|||
<br> |
|||
|
|||
You can see real life example, how to use this node in those tutorials: |
|||
|
|||
- [Create and Clear Alarms{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/#node-a-filter-script) |
|||
- [Reply to RPC Calls{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#add-filter-script-node) |
|||
|
|||
<br> |
|||
<br> |
|||
|
|||
@ -0,0 +1,118 @@ |
|||
#### Message generator function |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
*function Generate(prevMsg, prevMetadata, prevMsgType): {msg: object, metadata: object, msgType: string}* |
|||
|
|||
JavaScript function generating new message using previous Message payload, Metadata and Message type as input arguments. |
|||
|
|||
**Parameters:** |
|||
|
|||
<ul> |
|||
<li><b>prevMsg:</b> <code>{[key: string]: any}</code> - is a previously generated Message payload key/value object. |
|||
</li> |
|||
<li><b>prevMetadata:</b> <code>{[key: string]: string}</code> - is a previously generated Message metadata key/value object. |
|||
</li> |
|||
<li><b>prevMsgType:</b> <code>string</code> - is a previously generated string Message type. See <a href="https://github.com/thingsboard/thingsboard/blob/ea039008b148453dfa166cf92bc40b26e487e660/ui-ngx/src/app/shared/models/rule-node.models.ts#L338" target="_blank">MessageType</a> enum for common used values. |
|||
</li> |
|||
</ul> |
|||
|
|||
**Returns:** |
|||
|
|||
Should return the object with the following structure: |
|||
|
|||
```javascript |
|||
{ |
|||
msg?: {[key: string]: any}, |
|||
metadata?: {[key: string]: string}, |
|||
msgType?: string |
|||
} |
|||
``` |
|||
|
|||
All fields in resulting object are optional and will be taken from previously generated Message if not specified. |
|||
|
|||
<div class="divider"></div> |
|||
|
|||
##### Examples |
|||
|
|||
* Generate message of type `POST_TELEMETRY_REQUEST` with random `temperature` value from `18` to `32`: |
|||
|
|||
```javascript |
|||
var temperature = 18 + Math.random() * 14; |
|||
// Round to at most 2 decimal places (optional) |
|||
temperature = Math.round( temperature * 100 ) / 100; |
|||
var msg = { temperature: temperature }; |
|||
var metadata = {}; |
|||
var msgType = "POST_TELEMETRY_REQUEST"; |
|||
|
|||
return { msg: msg, metadata: metadata, msgType: msgType }; |
|||
{:copy-code} |
|||
``` |
|||
|
|||
|
|||
<ul> |
|||
<li> |
|||
Generate message of type <code>POST_TELEMETRY_REQUEST</code> with <code>temp</code> value <code>42</code>, |
|||
<code>humidity</code> value <code>77</code><br> |
|||
and <strong>metadata</strong> with field <code>data</code> having value <code>40</code>: |
|||
</li> |
|||
</ul> |
|||
|
|||
```javascript |
|||
var msg = { temp: 42, humidity: 77 }; |
|||
var metadata = { data: 40 }; |
|||
var msgType = "POST_TELEMETRY_REQUEST"; |
|||
|
|||
return { msg: msg, metadata: metadata, msgType: msgType }; |
|||
{:copy-code} |
|||
``` |
|||
|
|||
<ul> |
|||
<li> |
|||
Generate message of type <code>POST_TELEMETRY_REQUEST</code> with <code>temperature</code> value<br> |
|||
increasing and decreasing linearly in the range from <code>18</code> to <code>32</code>: |
|||
</li> |
|||
</ul> |
|||
|
|||
```javascript |
|||
var lower = 18; |
|||
var upper = 32; |
|||
var isDecrement = 'false'; |
|||
var temperature = lower; |
|||
|
|||
// Get previous values |
|||
|
|||
if (typeof prevMetadata !== 'undefined' && |
|||
typeof prevMetadata.isDecrement !== 'undefined') { |
|||
isDecrement = prevMetadata.isDecrement; |
|||
} |
|||
if (typeof prevMsg !== 'undefined' && |
|||
typeof prevMsg.temperature !== 'undefined') { |
|||
temperature = prevMsg.temperature; |
|||
} |
|||
|
|||
if (isDecrement === 'true') { |
|||
temperature--; |
|||
if (temperature <= lower) { |
|||
isDecrement = 'false'; |
|||
temperature = lower; |
|||
} |
|||
} else { |
|||
temperature++; |
|||
if (temperature >= upper) { |
|||
isDecrement = 'true'; |
|||
temperature = upper; |
|||
} |
|||
} |
|||
|
|||
var msg = { temperature: temperature }; |
|||
var metadata = { isDecrement: isDecrement }; |
|||
var msgType = "POST_TELEMETRY_REQUEST"; |
|||
|
|||
return { msg: msg, metadata: metadata, msgType: msgType }; |
|||
{:copy-code} |
|||
``` |
|||
|
|||
<br> |
|||
<br> |
|||
@ -0,0 +1,37 @@ |
|||
#### Message to string function |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
*function toString(msg, metadata, msgType): string* |
|||
|
|||
JavaScript function transforming incoming Message to String for further logging to the server log file. |
|||
|
|||
**Parameters:** |
|||
|
|||
{% include rulenode/common_node_script_args %} |
|||
|
|||
**Returns:** |
|||
|
|||
Should return `string` value used for logging to the server log file. |
|||
|
|||
<div class="divider"></div> |
|||
|
|||
##### Examples |
|||
|
|||
* Create string message containing incoming message and incoming metadata values: |
|||
|
|||
```javascript |
|||
return 'Incoming message:\n' + JSON.stringify(msg) + |
|||
'\nIncoming metadata:\n' + JSON.stringify(metadata); |
|||
{:copy-code} |
|||
``` |
|||
|
|||
<br> |
|||
|
|||
You can see real life example, how to use this node in this tutorial: |
|||
|
|||
- [Reply to RPC Calls{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#log-unknown-request) |
|||
|
|||
<br> |
|||
<br> |
|||
@ -0,0 +1,96 @@ |
|||
#### Switch message function |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
*function Switch(msg, metadata, msgType): string[]* |
|||
|
|||
JavaScript function computing **an array of next Relation names** for incoming Message. |
|||
|
|||
**Parameters:** |
|||
|
|||
{% include rulenode/common_node_script_args %} |
|||
|
|||
**Returns:** |
|||
|
|||
Should return an array of `string` values presenting **next Relation names** where Message should be routed.<br> |
|||
If returned array is empty - message will not be routed to any Node and discarded. |
|||
|
|||
<div class="divider"></div> |
|||
|
|||
##### Examples |
|||
|
|||
<ul> |
|||
<li> |
|||
Forward all messages with <code>temperature</code> value greater than <code>30</code> to the <strong>'High temperature'</strong> chain,<br> |
|||
with <code>temperature</code> value lower than <code>20</code> to the <strong>'Low temperature'</strong> chain and all other messages<br> |
|||
to the <strong>'Normal temperature'</strong> chain: |
|||
</li> |
|||
</ul> |
|||
|
|||
```javascript |
|||
if (msg.temperature > 30) { |
|||
return ['High temperature']; |
|||
} else if (msg.temperature < 20) { |
|||
return ['Low temperature']; |
|||
} else { |
|||
return ['Normal temperature']; |
|||
} |
|||
{:copy-code} |
|||
``` |
|||
|
|||
<ul> |
|||
<li> |
|||
For messages with type <code>POST_TELEMETRY_REQUEST</code>: |
|||
<ul> |
|||
<li> |
|||
if <code>temperature</code> value lower than <code>18</code> forward to the <strong>'Low temperature telemetry'</strong> chain, |
|||
</li> |
|||
<li> |
|||
otherwise to the <strong>'Normal temperature telemetry'</strong> chain. |
|||
</li> |
|||
</ul> |
|||
For messages with type <code>POST_ATTRIBUTES_REQUEST</code>:<br> |
|||
<ul> |
|||
<li> |
|||
if <code>currentState</code> value is <code>IDLE</code> forward to the <strong>'Idle State'</strong> and <strong>'Update State Attribute'</strong> chains, |
|||
</li> |
|||
<li> |
|||
if <code>currentState</code> value is <code>RUNNING</code> forward to the <strong>'Running State'</strong> and <strong>'Update State Attribute'</strong> chains, |
|||
</li> |
|||
<li> |
|||
otherwise to the <strong>'Unknown State'</strong> chain. |
|||
</li> |
|||
</ul> |
|||
For all other message types - discard the message (do not route to any Node). |
|||
</li> |
|||
</ul> |
|||
|
|||
```javascript |
|||
if (msgType === 'POST_TELEMETRY_REQUEST') { |
|||
if (msg.temperature < 18) { |
|||
return ['Low Temperature Telemetry']; |
|||
} else { |
|||
return ['Normal Temperature Telemetry']; |
|||
} |
|||
} else if (msgType === 'POST_ATTRIBUTES_REQUEST') { |
|||
if (msg.currentState === 'IDLE') { |
|||
return ['Idle State', 'Update State Attribute']; |
|||
} else if (msg.currentState === 'RUNNING') { |
|||
return ['Running State', 'Update State Attribute']; |
|||
} else { |
|||
return ['Unknown State']; |
|||
} |
|||
} |
|||
return []; |
|||
{:copy-code} |
|||
``` |
|||
|
|||
<br> |
|||
|
|||
You can see real life example, how to use this node in this tutorial: |
|||
|
|||
- [Data function based on telemetry from 2 devices{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/function-based-on-telemetry-from-two-devices#delta-temperature-rule-chain) |
|||
|
|||
<br> |
|||
<br> |
|||
File diff suppressed because one or more lines are too long
@ -0,0 +1,26 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2021 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. |
|||
|
|||
--> |
|||
<ng-container #markdownContainer> |
|||
</ng-container> |
|||
<div *ngIf="error && !fallbackToPlainMarkdown" style="color: #f00; font-size: 14px; |
|||
line-height: 28px; |
|||
background: #efefef;"> |
|||
{{error}} |
|||
</div> |
|||
<div #fallbackElement [fxShow]="error && fallbackToPlainMarkdown" class="tb-markdown-view" [ngClass]="markdownClass" [ngStyle]="style"> |
|||
</div> |
|||
@ -0,0 +1,194 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 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.
|
|||
///
|
|||
|
|||
import { |
|||
ChangeDetectorRef, |
|||
Component, |
|||
ComponentFactory, |
|||
ComponentRef, ElementRef, |
|||
EventEmitter, |
|||
Inject, |
|||
Injector, |
|||
Input, OnChanges, |
|||
Output, |
|||
SimpleChanges, |
|||
Type, ViewChild, |
|||
ViewContainerRef |
|||
} from '@angular/core'; |
|||
import { HelpService } from '@core/services/help.service'; |
|||
import { MarkdownService, PrismPlugin } from 'ngx-markdown'; |
|||
import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; |
|||
import { isDefinedAndNotNull } from '@core/utils'; |
|||
import { Observable, of, ReplaySubject } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-markdown', |
|||
templateUrl: './markdown.component.html' |
|||
}) |
|||
export class TbMarkdownComponent implements OnChanges { |
|||
|
|||
@ViewChild('markdownContainer', {read: ViewContainerRef, static: true}) markdownContainer: ViewContainerRef; |
|||
@ViewChild('fallbackElement', {static: true}) fallbackElement: ElementRef<HTMLElement>; |
|||
|
|||
@Input() data: string | undefined; |
|||
|
|||
@Input() markdownClass: string | undefined; |
|||
|
|||
@Input() style: { [klass: string]: any } = {}; |
|||
|
|||
@Input() |
|||
get lineNumbers(): boolean { return this.lineNumbersValue; } |
|||
set lineNumbers(value: boolean) { this.lineNumbersValue = coerceBooleanProperty(value); } |
|||
|
|||
@Input() |
|||
get fallbackToPlainMarkdown(): boolean { return this.fallbackToPlainMarkdownValue; } |
|||
set fallbackToPlainMarkdown(value: boolean) { this.fallbackToPlainMarkdownValue = coerceBooleanProperty(value); } |
|||
|
|||
@Output() ready = new EventEmitter<void>(); |
|||
|
|||
private lineNumbersValue = false; |
|||
private fallbackToPlainMarkdownValue = false; |
|||
|
|||
isMarkdownReady = false; |
|||
|
|||
error = null; |
|||
|
|||
private tbMarkdownInstanceComponentRef: ComponentRef<any>; |
|||
private tbMarkdownInstanceComponentFactory: ComponentFactory<any>; |
|||
|
|||
constructor(private help: HelpService, |
|||
private cd: ChangeDetectorRef, |
|||
public markdownService: MarkdownService, |
|||
@Inject(SHARED_MODULE_TOKEN) private sharedModule: Type<any>, |
|||
private dynamicComponentFactoryService: DynamicComponentFactoryService) {} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
if (isDefinedAndNotNull(this.data)) { |
|||
this.render(this.data); |
|||
} |
|||
} |
|||
|
|||
private render(markdown: string) { |
|||
const compiled = this.markdownService.compile(markdown, false); |
|||
let template = this.sanitizeCurlyBraces(compiled); |
|||
let markdownClass = 'tb-markdown-view'; |
|||
if (this.markdownClass) { |
|||
markdownClass += ` ${this.markdownClass}`; |
|||
} |
|||
template = `<div [ngStyle]="style" class="${markdownClass}">${template}</div>`; |
|||
this.markdownContainer.clear(); |
|||
const parent = this; |
|||
let readyObservable: Observable<void>; |
|||
this.dynamicComponentFactoryService.createDynamicComponentFactory( |
|||
class TbMarkdownInstance { |
|||
ngOnDestroy(): void { |
|||
parent.destroyMarkdownInstanceResources(); |
|||
} |
|||
}, |
|||
template, |
|||
[this.sharedModule], |
|||
true |
|||
).subscribe((factory) => { |
|||
this.tbMarkdownInstanceComponentFactory = factory; |
|||
const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector}); |
|||
try { |
|||
this.tbMarkdownInstanceComponentRef = |
|||
this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector); |
|||
this.tbMarkdownInstanceComponentRef.instance.style = this.style; |
|||
this.handlePlugins(this.tbMarkdownInstanceComponentRef.location.nativeElement); |
|||
this.markdownService.highlight(this.tbMarkdownInstanceComponentRef.location.nativeElement); |
|||
readyObservable = this.handleImages(this.tbMarkdownInstanceComponentRef.location.nativeElement); |
|||
this.error = null; |
|||
} catch (error) { |
|||
readyObservable = this.handleError(compiled, error); |
|||
} |
|||
this.cd.detectChanges(); |
|||
readyObservable.subscribe(() => { |
|||
this.ready.emit(); |
|||
}); |
|||
}, |
|||
(error) => { |
|||
readyObservable = this.handleError(compiled, error); |
|||
this.cd.detectChanges(); |
|||
readyObservable.subscribe(() => { |
|||
this.ready.emit(); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
private handleError(template: string, error): Observable<void> { |
|||
this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '<br>'); |
|||
this.destroyMarkdownInstanceResources(); |
|||
if (this.fallbackToPlainMarkdownValue) { |
|||
this.markdownContainer.clear(); |
|||
const element = this.fallbackElement.nativeElement; |
|||
element.innerHTML = template; |
|||
this.handlePlugins(element); |
|||
this.markdownService.highlight(element); |
|||
return this.handleImages(element); |
|||
} else { |
|||
return of(null); |
|||
} |
|||
} |
|||
|
|||
private handlePlugins(element: HTMLElement): void { |
|||
if (this.lineNumbers) { |
|||
this.setPluginClass(element, PrismPlugin.LineNumbers); |
|||
} |
|||
} |
|||
|
|||
private setPluginClass(element: HTMLElement, plugin: string | string[]): void { |
|||
const preElements = element.querySelectorAll('pre'); |
|||
for (let i = 0; i < preElements.length; i++) { |
|||
const classes = plugin instanceof Array ? plugin : [plugin]; |
|||
preElements.item(i).classList.add(...classes); |
|||
} |
|||
} |
|||
|
|||
private handleImages(element: HTMLElement): Observable<void> { |
|||
const imgs = $('img', element); |
|||
if (imgs.length) { |
|||
let totalImages = imgs.length; |
|||
const imagesLoadedSubject = new ReplaySubject<void>(); |
|||
imgs.each((index, img) => { |
|||
$(img).one('load error', () => { |
|||
totalImages--; |
|||
if (totalImages === 0) { |
|||
imagesLoadedSubject.next(); |
|||
imagesLoadedSubject.complete(); |
|||
} |
|||
}); |
|||
}); |
|||
return imagesLoadedSubject.asObservable(); |
|||
} else { |
|||
return of(null); |
|||
} |
|||
} |
|||
|
|||
private sanitizeCurlyBraces(template: string): string { |
|||
return template.replace(/{/g, '{').replace(/}/g, '}'); |
|||
} |
|||
|
|||
private destroyMarkdownInstanceResources() { |
|||
if (this.tbMarkdownInstanceComponentFactory) { |
|||
this.dynamicComponentFactoryService.destroyDynamicComponentFactory(this.tbMarkdownInstanceComponentFactory); |
|||
this.tbMarkdownInstanceComponentFactory = null; |
|||
} |
|||
this.tbMarkdownInstanceComponentRef = null; |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue