diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index 1de1bae6f1..130c0c6dd5 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -18,8 +18,8 @@ "resources": [], "templateHtml": "
", "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n", - "controllerScript": "var requestTimeout = 500;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"requestTimeout\"\n ]\n}", + "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"requestTimeout\",\n \"requestPersistent\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#010101\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"RPC debug terminal\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } diff --git a/application/src/main/data/upgrade/3.2.2/schema_update_event.sql b/application/src/main/data/upgrade/3.2.2/schema_update_event.sql new file mode 100644 index 0000000000..9b673bc7b6 --- /dev/null +++ b/application/src/main/data/upgrade/3.2.2/schema_update_event.sql @@ -0,0 +1,90 @@ +-- +-- 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. +-- + +-- PROCEDURE: public.cleanup_events_by_ttl(bigint, bigint, bigint) + +DROP PROCEDURE IF EXISTS public.cleanup_events_by_ttl(bigint, bigint, bigint); + +CREATE OR REPLACE PROCEDURE public.cleanup_events_by_ttl( + ttl bigint, + debug_ttl bigint, + INOUT deleted bigint) +LANGUAGE 'plpgsql' +AS $BODY$ +DECLARE + ttl_ts bigint; + debug_ttl_ts bigint; + ttl_deleted_count bigint DEFAULT 0; + debug_ttl_deleted_count bigint DEFAULT 0; +BEGIN + IF ttl > 0 THEN + ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - ttl::bigint * 1000)::bigint; + + DELETE FROM event + WHERE ts < ttl_ts + AND NOT event_type IN ('DEBUG_RULE_NODE', 'DEBUG_RULE_CHAIN', 'DEBUG_CONVERTER', 'DEBUG_INTEGRATION'); + + GET DIAGNOSTICS ttl_deleted_count = ROW_COUNT; + END IF; + + IF debug_ttl > 0 THEN + debug_ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - debug_ttl::bigint * 1000)::bigint; + + DELETE FROM event + WHERE ts < debug_ttl_ts + AND event_type IN ('DEBUG_RULE_NODE', 'DEBUG_RULE_CHAIN', 'DEBUG_CONVERTER', 'DEBUG_INTEGRATION'); + + GET DIAGNOSTICS debug_ttl_deleted_count = ROW_COUNT; + END IF; + + RAISE NOTICE 'Events removed by ttl: %', ttl_deleted_count; + RAISE NOTICE 'Debug Events removed by ttl: %', debug_ttl_deleted_count; + deleted := ttl_deleted_count + debug_ttl_deleted_count; +END +$BODY$; + + +-- Index: idx_event_ts + +DROP INDEX IF EXISTS public.idx_event_ts; + +-- Hint: add CONCURRENTLY to CREATE INDEX query in case of more then 1 million records or during live update +-- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_event_ts +CREATE INDEX IF NOT EXISTS idx_event_ts + ON public.event + (ts DESC NULLS LAST) + WITH (FILLFACTOR=95); + +COMMENT ON INDEX public.idx_event_ts + IS 'This index helps to delete events by TTL using timestamp'; + + +-- Index: idx_event_tenant_entity_type_entity_event_type_created_time_des + +DROP INDEX IF EXISTS public.idx_event_tenant_entity_type_entity_event_type_created_time_des; + +-- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_event_tenant_entity_type_entity_event_type_created_time_des +CREATE INDEX IF NOT EXISTS idx_event_tenant_entity_type_entity_event_type_created_time_des + ON public.event + (tenant_id ASC, entity_type ASC, entity_id ASC, event_type ASC, created_time DESC NULLS LAST) + WITH (FILLFACTOR=95); + +COMMENT ON INDEX public.idx_event_tenant_entity_type_entity_event_type_created_time_des + IS 'This index helps to open latest events on UI fast'; + +-- Index: idx_event_type_entity_id +-- Description: replaced with more suitable idx_event_tenant_entity_type_entity_event_type_created_time_des +DROP INDEX IF EXISTS public.idx_event_type_entity_id; \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index ec4b79408b..30eb24a4d2 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -206,13 +206,13 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { syncSessionSet.forEach(rpcSubscriptions::remove); } - if (persisted && !(sent || request.isOneway())) { + if (persisted) { ObjectNode response = JacksonUtil.newObjectNode(); response.put("rpcId", request.getId().toString()); systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(msg.getMsg().getId(), JacksonUtil.toString(response), null)); } - if (request.isOneway() && sent) { + if (!persisted && request.isOneway() && sent) { log.debug("[{}] Rpc command response sent [{}]!", deviceId, request.getId()); systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(msg.getMsg().getId(), null, null)); } else { @@ -233,7 +233,6 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { rpc.setExpirationTime(request.getExpirationTime()); rpc.setRequest(JacksonUtil.valueToTree(request)); rpc.setStatus(status); - systemContext.getTbRpcService().save(tenantId, rpc); return systemContext.getTbRpcService().save(tenantId, rpc); } @@ -298,7 +297,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { toDeviceRpcPendingMap.entrySet().stream().findFirst().ifPresent(processPendingRpc(context, sessionId, sessionInfo.getNodeId(), sentOneWayIds)); } - sentOneWayIds.forEach(toDeviceRpcPendingMap::remove); + sentOneWayIds.stream().filter(id -> !toDeviceRpcPendingMap.get(id).getMsg().getMsg().isPersisted()).forEach(toDeviceRpcPendingMap::remove); } private Consumer> processPendingRpc(TbActorCtx context, UUID sessionId, String nodeId, Set sentOneWayIds) { @@ -503,9 +502,6 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { } } else { log.debug("[{}] Rpc command response [{}] is stale!", deviceId, responseMsg.getRequestId()); - if (requestMd.getMsg().getMsg().isPersisted()) { - systemContext.getTbRpcService().save(tenantId, new RpcId(requestMd.getMsg().getMsg().getId()), RpcStatus.FAILED, JacksonUtil.toJsonNode(responseMsg.getPayload())); - } } } diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcController.java b/application/src/main/java/org/thingsboard/server/controller/RpcController.java index eae26542fa..50ab5c7863 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcController.java @@ -34,6 +34,7 @@ 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.rule.engine.api.RpcError; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -100,7 +101,7 @@ public class RpcController extends BaseController { } @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/persisted/{rpcId}", method = RequestMethod.GET) + @RequestMapping(value = "/persistent/{rpcId}", method = RequestMethod.GET) @ResponseBody public Rpc getPersistedRpc(@PathVariable("rpcId") String strRpc) throws ThingsboardException { checkParameter("RpcId", strRpc); @@ -113,7 +114,7 @@ public class RpcController extends BaseController { } @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/persisted/{deviceId}", method = RequestMethod.GET) + @RequestMapping(value = "/persistent/device/{deviceId}", method = RequestMethod.GET) @ResponseBody public PageData getPersistedRpcByDevice(@PathVariable("deviceId") String strDeviceId, @RequestParam int pageSize, @@ -134,7 +135,7 @@ public class RpcController extends BaseController { } @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/persisted/{rpcId}", method = RequestMethod.DELETE) + @RequestMapping(value = "/persistent/{rpcId}", method = RequestMethod.DELETE) @ResponseBody public void deleteResource(@PathVariable("rpcId") String strRpc) throws ThingsboardException { checkParameter("RpcId", strRpc); @@ -155,7 +156,7 @@ public class RpcController extends BaseController { long timeout = rpcRequestBody.has("timeout") ? rpcRequestBody.get("timeout").asLong() : defaultTimeout; long expTime = System.currentTimeMillis() + Math.max(minTimeout, timeout); UUID rpcRequestUUID = rpcRequestBody.has("requestUUID") ? UUID.fromString(rpcRequestBody.get("requestUUID").asText()) : UUID.randomUUID(); - boolean persisted = rpcRequestBody.has("persisted") && rpcRequestBody.get("persisted").asBoolean(); + boolean persisted = rpcRequestBody.has(DataConstants.PERSISTENT) && rpcRequestBody.get(DataConstants.PERSISTENT).asBoolean(); accessValidator.validate(currentUser, Operation.RPC_CALL, deviceId, new HttpValidationCallback(response, new FutureCallback>() { @Override public void onSuccess(@Nullable DeferredResult result) { diff --git a/application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java index 6bf3f4822d..e47a8ae0da 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.install; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import org.thingsboard.server.dao.util.PsqlDao; @@ -22,9 +23,22 @@ import org.thingsboard.server.dao.util.PsqlDao; @Service @PsqlDao @Profile("install") +@Slf4j public class PsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService implements EntityDatabaseSchemaService { + public static final String SCHEMA_ENTITIES_SQL = "schema-entities.sql"; + public static final String SCHEMA_ENTITIES_IDX_SQL = "schema-entities-idx.sql"; + public static final String SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL = "schema-entities-idx-psql-addon.sql"; + public PsqlEntityDatabaseSchemaService() { - super("schema-entities.sql", "schema-entities-idx.sql"); + super(SCHEMA_ENTITIES_SQL, SCHEMA_ENTITIES_IDX_SQL); + } + + @Override + public void createDatabaseIndexes() throws Exception { + super.createDatabaseIndexes(); + log.info("Installing SQL DataBase schema PostgreSQL specific indexes part: " + SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL); + executeQueryFromFile(SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java index 1e652880a0..ec56cc39b4 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java @@ -19,7 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import java.nio.charset.Charset; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -59,14 +59,8 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema @Override public void createDatabaseSchema(boolean createIndexes) throws Exception { - log.info("Installing SQL DataBase schema part: " + schemaSql); - - Path schemaFile = Paths.get(installScripts.getDataDir(), SQL_DIR, schemaSql); - try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { - String sql = new String(Files.readAllBytes(schemaFile), Charset.forName("UTF-8")); - conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to load initial thingsboard database schema - } + executeQueryFromFile(schemaSql); if (createIndexes) { this.createDatabaseIndexes(); @@ -77,11 +71,15 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema public void createDatabaseIndexes() throws Exception { if (schemaIdxSql != null) { log.info("Installing SQL DataBase schema indexes part: " + schemaIdxSql); - Path schemaIdxFile = Paths.get(installScripts.getDataDir(), SQL_DIR, schemaIdxSql); - try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { - String sql = new String(Files.readAllBytes(schemaIdxFile), Charset.forName("UTF-8")); - conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to load initial thingsboard database schema - } + executeQueryFromFile(schemaIdxSql); + } + } + + void executeQueryFromFile(String schemaIdxSql) throws SQLException, IOException { + Path schemaIdxFile = Paths.get(installScripts.getDataDir(), SQL_DIR, schemaIdxSql); + String sql = Files.readString(schemaIdxFile); + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to load initial thingsboard database schema } } @@ -91,7 +89,8 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema log.info("Successfully executed query: {}", query); Thread.sleep(5000); } catch (InterruptedException | SQLException e) { - log.info("Failed to execute query: {} due to: {}", query, e.getMessage()); + log.error("Failed to execute query: {} due to: {}", query, e.getMessage()); + throw new RuntimeException("Failed to execute query: " + query, e); } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 470f39f401..fcb9632020 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -459,6 +459,10 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.2.2", "schema_update_ttl.sql"); loadSql(schemaUpdateFile, conn); log.info("Edge TTL functions successfully loaded!"); + log.info("Updating indexes and TTL procedure for event table..."); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.2.2", "schema_update_event.sql"); + loadSql(schemaUpdateFile, conn); + log.info("Updating schema settings..."); conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003000;"); log.info("Schema updated."); } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 5ef7cc1942..0b261436b4 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -31,6 +31,7 @@ import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; import org.thingsboard.rule.engine.api.MailService; +import org.thingsboard.rule.engine.api.TbEmail; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.ApiFeature; import org.thingsboard.server.common.data.ApiUsageRecordKey; @@ -250,35 +251,35 @@ public class DefaultMailService implements MailService { } @Override - public void send(TenantId tenantId, CustomerId customerId, String from, String to, String cc, String bcc, String subject, String body, boolean isHtml, Map images) throws ThingsboardException { - sendMail(tenantId, customerId, from, to, cc, bcc, subject, body, isHtml, images, this.mailSender); + public void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail) throws ThingsboardException { + sendMail(tenantId, customerId, tbEmail, this.mailSender); } @Override - public void send(TenantId tenantId, CustomerId customerId, String from, String to, String cc, String bcc, String subject, String body, boolean isHtml, Map images, JavaMailSender javaMailSender) throws ThingsboardException { - sendMail(tenantId, customerId, from, to, cc, bcc, subject, body, isHtml, images, javaMailSender); + public void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail, JavaMailSender javaMailSender) throws ThingsboardException { + sendMail(tenantId, customerId, tbEmail, javaMailSender); } - private void sendMail(TenantId tenantId, CustomerId customerId, String from, String to, String cc, String bcc, String subject, String body, boolean isHtml, Map images, JavaMailSender javaMailSender) throws ThingsboardException { + private void sendMail(TenantId tenantId, CustomerId customerId, TbEmail tbEmail, JavaMailSender javaMailSender) throws ThingsboardException { if (apiUsageStateService.getApiUsageState(tenantId).isEmailSendEnabled()) { try { MimeMessage mailMsg = javaMailSender.createMimeMessage(); - boolean multipart = (images != null && !images.isEmpty()); + boolean multipart = (tbEmail.getImages() != null && !tbEmail.getImages().isEmpty()); MimeMessageHelper helper = new MimeMessageHelper(mailMsg, multipart, "UTF-8"); - helper.setFrom(StringUtils.isBlank(from) ? mailFrom : from); - helper.setTo(to.split("\\s*,\\s*")); - if (!StringUtils.isBlank(cc)) { - helper.setCc(cc.split("\\s*,\\s*")); + helper.setFrom(StringUtils.isBlank(tbEmail.getFrom()) ? mailFrom : tbEmail.getFrom()); + helper.setTo(tbEmail.getTo().split("\\s*,\\s*")); + if (!StringUtils.isBlank(tbEmail.getCc())) { + helper.setCc(tbEmail.getCc().split("\\s*,\\s*")); } - if (!StringUtils.isBlank(bcc)) { - helper.setBcc(bcc.split("\\s*,\\s*")); + if (!StringUtils.isBlank(tbEmail.getBcc())) { + helper.setBcc(tbEmail.getBcc().split("\\s*,\\s*")); } - helper.setSubject(subject); - helper.setText(body, isHtml); + helper.setSubject(tbEmail.getSubject()); + helper.setText(tbEmail.getBody(), tbEmail.isHtml()); if (multipart) { - for (String imgId : images.keySet()) { - String imgValue = images.get(imgId); + for (String imgId : tbEmail.getImages().keySet()) { + String imgValue = tbEmail.getImages().get(imgId); String value = imgValue.replaceFirst("^data:image/[^;]*;base64,?", ""); byte[] bytes = javax.xml.bind.DatatypeConverter.parseBase64Binary(value); String contentType = helper.getFileTypeMap().getContentType(imgId); diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java index a69402201a..1ef8d9361e 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java @@ -157,7 +157,7 @@ public class DefaultTbCoreDeviceRpcService implements TbCoreDeviceRpcService { metaData.putValue("originServiceId", serviceId); metaData.putValue("expirationTime", Long.toString(msg.getExpirationTime())); metaData.putValue("oneway", Boolean.toString(msg.isOneway())); - metaData.putValue("persisted", Boolean.toString(msg.isPersisted())); + metaData.putValue(DataConstants.PERSISTENT, Boolean.toString(msg.isPersisted())); Device device = deviceService.findDeviceById(msg.getTenantId(), msg.getDeviceId()); if (device != null) { diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 56fb65c4e2..4d3e668d61 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -462,9 +462,12 @@ public class DefaultTransportApiService implements TransportApiService { private DeviceInfoProto getDeviceInfoProto(Device device) throws JsonProcessingException { PowerMode powerMode = null; + Long edrxCycle = null; switch (device.getDeviceData().getTransportConfiguration().getType()) { case LWM2M: - powerMode = ((Lwm2mDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration()).getPowerMode(); + Lwm2mDeviceTransportConfiguration transportConfiguration = (Lwm2mDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration(); + powerMode = transportConfiguration.getPowerMode(); + edrxCycle = transportConfiguration.getEdrxCycle(); break; } @@ -482,6 +485,7 @@ public class DefaultTransportApiService implements TransportApiService { .setAdditionalInfo(mapper.writeValueAsString(device.getAdditionalInfo())); if (powerMode != null) { builder.setPowerMode(powerMode.name()); + builder.setEdrxCycle(edrxCycle); } return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/EventsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/EventsCleanUpService.java index a51910b7ed..34cb2e1320 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/EventsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/EventsCleanUpService.java @@ -29,6 +29,9 @@ import org.thingsboard.server.service.ttl.AbstractCleanUpService; @Service public class EventsCleanUpService extends AbstractCleanUpService { + public static final String RANDOM_DELAY_INTERVAL_MS_EXPRESSION = + "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.events.execution_interval_ms})}"; + @Value("${sql.ttl.events.events_ttl}") private long ttl; @@ -45,7 +48,7 @@ public class EventsCleanUpService extends AbstractCleanUpService { this.eventService = eventService; } - @Scheduled(initialDelayString = "${sql.ttl.events.execution_interval_ms}", fixedDelayString = "${sql.ttl.events.execution_interval_ms}") + @Scheduled(initialDelayString = RANDOM_DELAY_INTERVAL_MS_EXPRESSION, fixedDelayString = "${sql.ttl.events.execution_interval_ms}") public void cleanUp() { if (ttlTaskExecutionEnabled && isSystemTenantPartitionMine()) { eventService.cleanupEvents(ttl, debugTtl); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 89adef5456..4fd605e2f2 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -266,7 +266,7 @@ sql: ts_key_value_ttl: "${SQL_TTL_TS_TS_KEY_VALUE_TTL:0}" # Number of seconds events: enabled: "${SQL_TTL_EVENTS_ENABLED:true}" - execution_interval_ms: "${SQL_TTL_EVENTS_EXECUTION_INTERVAL:86400000}" # Number of milliseconds. The current value corresponds to one day + execution_interval_ms: "${SQL_TTL_EVENTS_EXECUTION_INTERVAL:2220000}" # Number of milliseconds (max random initial delay and fixed period). # 37minutes to avoid common interval spikes events_ttl: "${SQL_TTL_EVENTS_EVENTS_TTL:0}" # Number of seconds debug_events_ttl: "${SQL_TTL_EVENTS_DEBUG_EVENTS_TTL:604800}" # Number of seconds. The current value corresponds to one week edge_events: diff --git a/application/src/test/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaServiceTest.java b/application/src/test/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaServiceTest.java new file mode 100644 index 0000000000..eb08390ec7 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaServiceTest.java @@ -0,0 +1,54 @@ +/** + * 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.service.install; + +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class PsqlEntityDatabaseSchemaServiceTest { + + @Test + public void givenPsqlDbSchemaService_whenCreateDatabaseSchema_thenVerifyPsqlIndexSpecificCall() throws Exception { + PsqlEntityDatabaseSchemaService service = spy(new PsqlEntityDatabaseSchemaService()); + willDoNothing().given(service).executeQueryFromFile(anyString()); + + service.createDatabaseSchema(); + + verify(service, times(1)).createDatabaseIndexes(); + verify(service, times(1)).executeQueryFromFile(PsqlEntityDatabaseSchemaService.SCHEMA_ENTITIES_SQL); + verify(service, times(1)).executeQueryFromFile(PsqlEntityDatabaseSchemaService.SCHEMA_ENTITIES_IDX_SQL); + verify(service, times(1)).executeQueryFromFile(PsqlEntityDatabaseSchemaService.SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL); + verify(service, times(3)).executeQueryFromFile(anyString()); + } + + @Test + public void givenPsqlDbSchemaService_whenCreateDatabaseIndexes_thenVerifyPsqlIndexSpecificCall() throws Exception { + PsqlEntityDatabaseSchemaService service = spy(new PsqlEntityDatabaseSchemaService()); + willDoNothing().given(service).executeQueryFromFile(anyString()); + + service.createDatabaseIndexes(); + + verify(service, times(1)).executeQueryFromFile(PsqlEntityDatabaseSchemaService.SCHEMA_ENTITIES_IDX_SQL); + verify(service, times(1)).executeQueryFromFile(PsqlEntityDatabaseSchemaService.SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL); + verify(service, times(2)).executeQueryFromFile(anyString()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/ttl/EventsCleanUpServiceTest.java b/application/src/test/java/org/thingsboard/server/service/ttl/EventsCleanUpServiceTest.java new file mode 100644 index 0000000000..422d5ef358 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/ttl/EventsCleanUpServiceTest.java @@ -0,0 +1,50 @@ +/** + * 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.service.ttl; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.thingsboard.server.service.ttl.EventsCleanUpService.RANDOM_DELAY_INTERVAL_MS_EXPRESSION; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = EventsCleanUpServiceTest.class) +@Slf4j +public class EventsCleanUpServiceTest { + + @Value(RANDOM_DELAY_INTERVAL_MS_EXPRESSION) + long randomDelayMs; + @Value("${sql.ttl.events.execution_interval_ms}") + long executionIntervalMs; + + @Test + public void givenInterval_whenRandomDelay_ThenDelayInInterval() { + log.info("randomDelay {}", randomDelayMs); + log.info("executionIntervalMs {}", executionIntervalMs); + assertThat(executionIntervalMs, is(2220000L)); + assertThat(randomDelayMs, greaterThanOrEqualTo(0L)); + assertThat(randomDelayMs, lessThanOrEqualTo(executionIntervalMs)); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java index a4dba5bb74..382f5595a5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.transport.coap.AbstractCoapIntegrationTest; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index efc81846d5..3b25bd1290 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -35,6 +35,7 @@ public class DataConstants { public static final String IS_CLEARED_ALARM = "isClearedAlarm"; public static final String ALARM_CONDITION_REPEATS = "alarmConditionRepeats"; public static final String ALARM_CONDITION_DURATION = "alarmConditionDuration"; + public static final String PERSISTENT = "persistent"; public static final String[] allScopes() { return new String[]{CLIENT_SCOPE, SHARED_SCOPE, SERVER_SCOPE}; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java index 085e60212a..2bcd16ae52 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java @@ -29,6 +29,8 @@ public class Lwm2mDeviceTransportConfiguration implements DeviceTransportConfigu private PowerMode powerMode; + private Long edrxCycle; + @JsonIgnore private Map properties = new HashMap<>(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/lwm2m/OtherConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/lwm2m/OtherConfiguration.java index 1a9f2784da..c7681cfad8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/lwm2m/OtherConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/lwm2m/OtherConfiguration.java @@ -27,6 +27,7 @@ public class OtherConfiguration { private Integer swUpdateStrategy; private Integer clientOnlyObserveAfterConnect; private PowerMode powerMode; + private Long edrxCycle; private String fwUpdateResource; private String swUpdateResource; private boolean compositeOperationsSupport; diff --git a/common/queue/src/main/proto/queue.proto b/common/queue/src/main/proto/queue.proto index 1225e6d824..9b09bf2328 100644 --- a/common/queue/src/main/proto/queue.proto +++ b/common/queue/src/main/proto/queue.proto @@ -116,6 +116,7 @@ message DeviceInfoProto { int64 customerIdMSB = 10; int64 customerIdLSB = 11; string powerMode = 12; + int64 edrxCycle = 13; } /** diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/AbstractCoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/AbstractCoapTransportResource.java index 7ab1306953..83a6d2ff11 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/AbstractCoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/AbstractCoapTransportResource.java @@ -15,11 +15,14 @@ */ package org.thingsboard.server.transport.coap; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.MessageObserver; import org.eclipse.californium.core.coap.Response; import org.eclipse.californium.core.server.resources.CoapExchange; +import org.eclipse.californium.elements.EndpointContext; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.transport.TransportContext; import org.thingsboard.server.common.transport.TransportService; @@ -29,8 +32,12 @@ import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsRes import org.thingsboard.server.gen.transport.TransportProtos; import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; import java.util.function.BiConsumer; +import static org.eclipse.californium.core.coap.Message.MAX_MID; +import static org.eclipse.californium.core.coap.Message.NONE; + @Slf4j public abstract class AbstractCoapTransportResource extends CoapResource { @@ -75,77 +82,8 @@ public abstract class AbstractCoapTransportResource extends CoapResource { .setEvent(event).build(); } - public static class CoapDeviceAuthCallback implements TransportServiceCallback { - private final TransportContext transportContext; - private final CoapExchange exchange; - private final BiConsumer onSuccess; - - public CoapDeviceAuthCallback(TransportContext transportContext, CoapExchange exchange, BiConsumer onSuccess) { - this.transportContext = transportContext; - this.exchange = exchange; - this.onSuccess = onSuccess; - } - - @Override - public void onSuccess(ValidateDeviceCredentialsResponse msg) { - DeviceProfile deviceProfile = msg.getDeviceProfile(); - if (msg.hasDeviceInfo() && deviceProfile != null) { - TransportProtos.SessionInfoProto sessionInfoProto = SessionInfoCreator.create(msg, transportContext, UUID.randomUUID()); - onSuccess.accept(sessionInfoProto, deviceProfile); - } else { - exchange.respond(CoAP.ResponseCode.UNAUTHORIZED); - } - } - - @Override - public void onError(Throwable e) { - log.warn("Failed to process request", e); - exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); - } - } - - public static class CoapOkCallback implements TransportServiceCallback { - private final CoapExchange exchange; - private final CoAP.ResponseCode onSuccessResponse; - private final CoAP.ResponseCode onFailureResponse; - - public CoapOkCallback(CoapExchange exchange, CoAP.ResponseCode onSuccessResponse, CoAP.ResponseCode onFailureResponse) { - this.exchange = exchange; - this.onSuccessResponse = onSuccessResponse; - this.onFailureResponse = onFailureResponse; - } - - @Override - public void onSuccess(Void msg) { - Response response = new Response(onSuccessResponse); - response.setAcknowledged(isConRequest()); - exchange.respond(response); - } - - @Override - public void onError(Throwable e) { - exchange.respond(onFailureResponse); - } - - private boolean isConRequest() { - return exchange.advanced().getRequest().isConfirmable(); - } + protected int getNextMsgId() { + return ThreadLocalRandom.current().nextInt(NONE, MAX_MID + 1); } - public static class CoapNoOpCallback implements TransportServiceCallback { - private final CoapExchange exchange; - - CoapNoOpCallback(CoapExchange exchange) { - this.exchange = exchange; - } - - @Override - public void onSuccess(Void msg) { - } - - @Override - public void onError(Throwable e) { - exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); - } - } } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java index 6dc0b6540a..8b60e3d3dc 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java @@ -22,10 +22,14 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportContext; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.coap.adaptors.JsonCoapAdaptor; import org.thingsboard.server.transport.coap.adaptors.ProtoCoapAdaptor; import org.thingsboard.server.transport.coap.efento.adaptor.EfentoCoapAdaptor; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + /** * Created by ashvayka on 18.10.18. @@ -33,22 +37,21 @@ import org.thingsboard.server.transport.coap.efento.adaptor.EfentoCoapAdaptor; @Slf4j @ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')") @Component +@Getter public class CoapTransportContext extends TransportContext { - @Getter @Value("${transport.sessions.report_timeout}") private long sessionReportTimeout; - @Getter @Autowired private JsonCoapAdaptor jsonCoapAdaptor; - @Getter @Autowired private ProtoCoapAdaptor protoCoapAdaptor; - @Getter @Autowired private EfentoCoapAdaptor efentoCoapAdaptor; + private final ConcurrentMap rpcAwaitingAck = new ConcurrentHashMap<>(); + } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index f5119570c9..449d371a2f 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -19,6 +19,7 @@ import com.google.gson.JsonParseException; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; import lombok.Data; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Request; @@ -53,6 +54,9 @@ import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor; +import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback; +import org.thingsboard.server.transport.coap.callback.CoapNoOpCallback; +import org.thingsboard.server.transport.coap.callback.CoapOkCallback; import java.util.List; import java.util.Map; @@ -81,9 +85,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource { private final Set rpcSubscriptions = ConcurrentHashMap.newKeySet(); private final Set attributeSubscriptions = ConcurrentHashMap.newKeySet(); - private ConcurrentMap dtlsSessionIdMap; - private long timeout; - private long sessionReportTimeout; + private final ConcurrentMap dtlsSessionIdMap; + private final long timeout; public CoapTransportResource(CoapTransportContext ctx, CoapServerService coapServerService, String name) { super(ctx, name); @@ -91,7 +94,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { this.addObserver(new CoapResourceObserver()); this.dtlsSessionIdMap = coapServerService.getDtlsSessionsMap(); this.timeout = coapServerService.getTimeout(); - this.sessionReportTimeout = ctx.getSessionReportTimeout(); + long sessionReportTimeout = ctx.getSessionReportTimeout(); ctx.getScheduler().scheduleAtFixedRate(() -> { Set coapObserveSessionInfos = sessionInfoToObserveRelationMap.keySet(); Set observeSessions = coapObserveSessionInfos @@ -110,7 +113,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource { return; // because request did not try to establish a relation } if (CoAP.ResponseCode.isSuccess(response.getCode())) { - if (!relation.isEstablished()) { relation.setEstablished(); addObserveRelation(relation); @@ -280,8 +282,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource { CoapObserveSessionInfo currentCoapObserveAttrSessionInfo = tokenToCoapSessionInfoMap.get(getTokenFromRequest(request)); if (currentCoapObserveAttrSessionInfo == null) { attributeSubscriptions.add(sessionId); - registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, - transportConfigurationContainer.getRpcRequestDynamicMessageBuilder(), getTokenFromRequest(request)); + registerAsyncCoapSession(exchange, coapTransportAdaptor, transportConfigurationContainer.getRpcRequestDynamicMessageBuilder(), + sessionInfo, getTokenFromRequest(request)); transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.getDefaultInstance(), new CoapNoOpCallback(exchange)); transportService.process(sessionInfo, @@ -305,11 +307,11 @@ public class CoapTransportResource extends AbstractCoapTransportResource { CoapObserveSessionInfo currentCoapObserveRpcSessionInfo = tokenToCoapSessionInfoMap.get(getTokenFromRequest(request)); if (currentCoapObserveRpcSessionInfo == null) { rpcSubscriptions.add(sessionId); - registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, - transportConfigurationContainer.getRpcRequestDynamicMessageBuilder(), getTokenFromRequest(request)); + registerAsyncCoapSession(exchange, coapTransportAdaptor, transportConfigurationContainer.getRpcRequestDynamicMessageBuilder() + , sessionInfo, getTokenFromRequest(request)); transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.getDefaultInstance(), - new CoapOkCallback(exchange, CoAP.ResponseCode.VALID, CoAP.ResponseCode.INTERNAL_SERVER_ERROR) + new CoapOkCallback(exchange, CoAP.ResponseCode.VALID, CoAP.ResponseCode.INTERNAL_SERVER_ERROR) ); } break; @@ -359,14 +361,16 @@ public class CoapTransportResource extends AbstractCoapTransportResource { return tokenToCoapSessionInfoMap.remove(token); } - private void registerAsyncCoapSession(CoapExchange exchange, TransportProtos.SessionInfoProto sessionInfo, CoapTransportAdaptor coapTransportAdaptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder, String token) { + private void registerAsyncCoapSession(CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor, + DynamicMessage.Builder rpcRequestDynamicMessageBuilder, TransportProtos.SessionInfoProto sessionInfo, String token) { tokenToCoapSessionInfoMap.putIfAbsent(token, new CoapObserveSessionInfo(sessionInfo)); transportService.registerAsyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor, rpcRequestDynamicMessageBuilder, sessionInfo)); transportService.process(sessionInfo, getSessionEventMsg(TransportProtos.SessionEvent.OPEN), null); } - private CoapSessionListener getCoapSessionListener(CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder, TransportProtos.SessionInfoProto sessionInfo) { - return new CoapSessionListener(this, exchange, coapTransportAdaptor, rpcRequestDynamicMessageBuilder, sessionInfo); + private CoapSessionListener getCoapSessionListener(CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor, + DynamicMessage.Builder rpcRequestDynamicMessageBuilder, TransportProtos.SessionInfoProto sessionInfo) { + return new CoapSessionListener(exchange, coapTransportAdaptor, rpcRequestDynamicMessageBuilder, sessionInfo); } private String getTokenFromRequest(Request request) { @@ -448,22 +452,14 @@ public class CoapTransportResource extends AbstractCoapTransportResource { } } - private static class CoapSessionListener implements SessionMsgListener { + @RequiredArgsConstructor + private class CoapSessionListener implements SessionMsgListener { - private final CoapTransportResource coapTransportResource; private final CoapExchange exchange; private final CoapTransportAdaptor coapTransportAdaptor; private final DynamicMessage.Builder rpcRequestDynamicMessageBuilder; private final TransportProtos.SessionInfoProto sessionInfo; - CoapSessionListener(CoapTransportResource coapTransportResource, CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder, TransportProtos.SessionInfoProto sessionInfo) { - this.coapTransportResource = coapTransportResource; - this.exchange = exchange; - this.coapTransportAdaptor = coapTransportAdaptor; - this.rpcRequestDynamicMessageBuilder = rpcRequestDynamicMessageBuilder; - this.sessionInfo = sessionInfo; - } - @Override public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg msg) { try { @@ -496,18 +492,30 @@ public class CoapTransportResource extends AbstractCoapTransportResource { @Override public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg msg) { log.trace("[{}] Received RPC command to device", sessionId); - boolean successful = true; try { - exchange.respond(coapTransportAdaptor.convertToPublish(isConRequest(), msg, rpcRequestDynamicMessageBuilder)); + Response response = coapTransportAdaptor.convertToPublish(isConRequest(), msg, rpcRequestDynamicMessageBuilder); + int requestId = getNextMsgId(); + response.setMID(requestId); + if (msg.getPersisted()) { + transportContext.getRpcAwaitingAck().put(requestId, msg); + transportContext.getScheduler().schedule(() -> { + TransportProtos.ToDeviceRpcRequestMsg awaitingAckMsg = transportContext.getRpcAwaitingAck().remove(requestId); + if (awaitingAckMsg != null) { + transportService.process(sessionInfo, msg, true, TransportServiceCallback.EMPTY); + } + }, Math.max(0, msg.getExpirationTime() - System.currentTimeMillis()), TimeUnit.MILLISECONDS); + } + response.addMessageObserver(new TbCoapMessageObserver(requestId, id -> { + TransportProtos.ToDeviceRpcRequestMsg rpcRequestMsg = transportContext.getRpcAwaitingAck().remove(id); + if (rpcRequestMsg != null) { + transportService.process(sessionInfo, rpcRequestMsg, false, TransportServiceCallback.EMPTY); + } + })); + exchange.respond(response); } catch (AdaptorException e) { log.trace("Failed to reply due to error", e); closeObserveRelationAndNotify(sessionId, CoAP.ResponseCode.INTERNAL_SERVER_ERROR); - successful = false; - } finally { - coapTransportResource.transportService.process(sessionInfo, msg, !successful, TransportServiceCallback.EMPTY); - if (!successful) { - closeAndDeregister(); - } + closeAndDeregister(); } } @@ -526,8 +534,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource { } private void closeObserveRelationAndNotify(UUID sessionId, CoAP.ResponseCode responseCode) { - Map sessionToObserveRelationMap = coapTransportResource.getCoapSessionInfoToObserveRelationMap(); - if (coapTransportResource.getObserverCount() > 0 && !CollectionUtils.isEmpty(sessionToObserveRelationMap)) { + Map sessionToObserveRelationMap = CoapTransportResource.this.getCoapSessionInfoToObserveRelationMap(); + if (CoapTransportResource.this.getObserverCount() > 0 && !CollectionUtils.isEmpty(sessionToObserveRelationMap)) { Optional observeSessionToClose = sessionToObserveRelationMap.keySet().stream().filter(coapObserveSessionInfo -> { TransportProtos.SessionInfoProto sessionToDelete = coapObserveSessionInfo.getSessionInfoProto(); UUID observeSessionId = new UUID(sessionToDelete.getSessionIdMSB(), sessionToDelete.getSessionIdLSB()); @@ -536,16 +544,16 @@ public class CoapTransportResource extends AbstractCoapTransportResource { if (observeSessionToClose.isPresent()) { CoapObserveSessionInfo coapObserveSessionInfo = observeSessionToClose.get(); ObserveRelation observeRelation = sessionToObserveRelationMap.get(coapObserveSessionInfo); - coapTransportResource.clearAndNotifyObserveRelation(observeRelation, responseCode); + CoapTransportResource.this.clearAndNotifyObserveRelation(observeRelation, responseCode); } } } private void closeAndDeregister() { Request request = exchange.advanced().getRequest(); - String token = coapTransportResource.getTokenFromRequest(request); - CoapObserveSessionInfo deleted = coapTransportResource.lookupAsyncSessionInfo(token); - coapTransportResource.closeAndDeregister(deleted.getSessionInfoProto()); + String token = CoapTransportResource.this.getTokenFromRequest(request); + CoapObserveSessionInfo deleted = CoapTransportResource.this.lookupAsyncSessionInfo(token); + CoapTransportResource.this.closeAndDeregister(deleted.getSessionInfoProto()); } } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java index 2aadea26b2..74777d59d4 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java @@ -20,22 +20,19 @@ import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Response; import org.eclipse.californium.core.network.Exchange; -import org.eclipse.californium.core.observe.ObserveRelation; import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.californium.core.server.resources.Resource; -import org.eclipse.californium.core.server.resources.ResourceObserver; -import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.security.DeviceTokenCredentials; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback; import java.util.List; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.ExecutorService; @Slf4j public class OtaPackageTransportResource extends AbstractCoapTransportResource { diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapMessageObserver.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapMessageObserver.java new file mode 100644 index 0000000000..c0ed1422c7 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapMessageObserver.java @@ -0,0 +1,95 @@ +/** + * 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.transport.coap; + +import lombok.RequiredArgsConstructor; +import org.eclipse.californium.core.coap.MessageObserver; +import org.eclipse.californium.core.coap.Response; +import org.eclipse.californium.elements.EndpointContext; + +import java.util.function.Consumer; + +@RequiredArgsConstructor +public class TbCoapMessageObserver implements MessageObserver { + + private final int msgId; + private final Consumer onAcknowledge; + + @Override + public void onRetransmission() { + + } + + @Override + public void onResponse(Response response) { + + } + + @Override + public void onAcknowledgement() { + onAcknowledge.accept(msgId); + } + + @Override + public void onReject() { + + } + + @Override + public void onTimeout() { + + } + + @Override + public void onCancel() { + + } + + @Override + public void onReadyToSend() { + + } + + @Override + public void onConnecting() { + + } + + @Override + public void onDtlsRetransmission(int flight) { + + } + + @Override + public void onSent(boolean retransmission) { + + } + + @Override + public void onSendError(Throwable error) { + + } + + @Override + public void onContextEstablished(EndpointContext endpointContext) { + + } + + @Override + public void onComplete() { + + } +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java index 4b4369f222..88b0be4634 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java @@ -150,7 +150,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { private Response getObserveNotification(boolean confirmable, JsonElement json) { Response response = new Response(CoAP.ResponseCode._UNKNOWN_SUCCESS_CODE); response.setPayload(json.toString()); - response.setAcknowledged(confirmable); + response.setConfirmable(confirmable); return response; } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java index 2ab377611c..1eeab38215 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java @@ -122,7 +122,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { @Override public Response convertToPublish(boolean isConfirmable, TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException { Response response = new Response(CoAP.ResponseCode.CONTENT); - response.setAcknowledged(isConfirmable); + response.setConfirmable(isConfirmable); response.setPayload(msg.toByteArray()); return response; } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapDeviceAuthCallback.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapDeviceAuthCallback.java new file mode 100644 index 0000000000..fab0c9b615 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapDeviceAuthCallback.java @@ -0,0 +1,60 @@ +/** + * 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.transport.coap.callback; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.transport.TransportContext; +import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.common.transport.auth.SessionInfoCreator; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.coap.AbstractCoapTransportResource; + +import java.util.UUID; +import java.util.function.BiConsumer; + +@Slf4j +public class CoapDeviceAuthCallback implements TransportServiceCallback { + private final TransportContext transportContext; + private final CoapExchange exchange; + private final BiConsumer onSuccess; + + public CoapDeviceAuthCallback(TransportContext transportContext, CoapExchange exchange, BiConsumer onSuccess) { + this.transportContext = transportContext; + this.exchange = exchange; + this.onSuccess = onSuccess; + } + + @Override + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + DeviceProfile deviceProfile = msg.getDeviceProfile(); + if (msg.hasDeviceInfo() && deviceProfile != null) { + TransportProtos.SessionInfoProto sessionInfoProto = SessionInfoCreator.create(msg, transportContext, UUID.randomUUID()); + onSuccess.accept(sessionInfoProto, deviceProfile); + } else { + exchange.respond(CoAP.ResponseCode.UNAUTHORIZED); + } + } + + @Override + public void onError(Throwable e) { + log.warn("Failed to process request", e); + exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); + } +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapNoOpCallback.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapNoOpCallback.java new file mode 100644 index 0000000000..4fb00496c3 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapNoOpCallback.java @@ -0,0 +1,37 @@ +/** + * 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.transport.coap.callback; + +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.thingsboard.server.common.transport.TransportServiceCallback; + +public class CoapNoOpCallback implements TransportServiceCallback { + private final CoapExchange exchange; + + public CoapNoOpCallback(CoapExchange exchange) { + this.exchange = exchange; + } + + @Override + public void onSuccess(Void msg) { + } + + @Override + public void onError(Throwable e) { + exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); + } +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java new file mode 100644 index 0000000000..40e1b894ee --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java @@ -0,0 +1,50 @@ +/** + * 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.transport.coap.callback; + +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.Response; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.thingsboard.server.common.transport.TransportServiceCallback; + +public class CoapOkCallback implements TransportServiceCallback { + + protected final CoapExchange exchange; + protected final CoAP.ResponseCode onSuccessResponse; + protected final CoAP.ResponseCode onFailureResponse; + + public CoapOkCallback(CoapExchange exchange, CoAP.ResponseCode onSuccessResponse, CoAP.ResponseCode onFailureResponse) { + this.exchange = exchange; + this.onSuccessResponse = onSuccessResponse; + this.onFailureResponse = onFailureResponse; + } + + @Override + public void onSuccess(Void msg) { + Response response = new Response(onSuccessResponse); + response.setConfirmable(isConRequest()); + exchange.respond(response); + } + + @Override + public void onError(Throwable e) { + exchange.respond(onFailureResponse); + } + + protected boolean isConRequest() { + return exchange.advanced().getRequest().isConfirmable(); + } +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java index d53cd7e49e..fe86d8794c 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java @@ -35,6 +35,8 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos; import org.thingsboard.server.gen.transport.coap.MeasurementsProtos; import org.thingsboard.server.transport.coap.AbstractCoapTransportResource; +import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback; +import org.thingsboard.server.transport.coap.callback.CoapOkCallback; import org.thingsboard.server.transport.coap.CoapTransportContext; import org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/attributes/DefaultLwM2MAttributesService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/attributes/DefaultLwM2MAttributesService.java index 353188c8ff..0d2ea53f8a 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/attributes/DefaultLwM2MAttributesService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/attributes/DefaultLwM2MAttributesService.java @@ -191,7 +191,7 @@ public class DefaultLwM2MAttributesService implements LwM2MAttributesService { private void pushUpdateToClientIfNeeded(LwM2mClient lwM2MClient, Object valueOld, Object newValue, String versionedId) { if (newValue != null && (valueOld == null || !newValue.toString().equals(valueOld.toString()))) { - TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(versionedId).value(newValue).timeout(this.config.getTimeout()).build(); + TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(versionedId).value(newValue).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); downlinkHandler.sendWriteReplaceRequest(lwM2MClient, request, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, lwM2MClient, versionedId)); } else { log.error("Failed update resource [{}] [{}]", versionedId, newValue); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index 2662560d08..a191849215 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -93,6 +93,8 @@ public class LwM2mClient implements Serializable { @Getter private PowerMode powerMode; @Getter + private Long edrxCycle; + @Getter @Setter private Registration registration; @@ -115,6 +117,7 @@ public class LwM2mClient implements Serializable { this.deviceId = new UUID(session.getDeviceIdMSB(), session.getDeviceIdLSB()); this.profileId = new UUID(session.getDeviceProfileIdMSB(), session.getDeviceProfileIdLSB()); this.powerMode = credentials.getDeviceInfo().getPowerMode(); + this.edrxCycle = credentials.getDeviceInfo().getEdrxCycle(); } public void lock() { @@ -133,7 +136,9 @@ public class LwM2mClient implements Serializable { builder.setDeviceName(device.getName()); deviceProfileOpt.ifPresent(deviceProfile -> updateSession(deviceProfile, builder)); this.session = builder.build(); - this.powerMode = ((Lwm2mDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration()).getPowerMode(); + Lwm2mDeviceTransportConfiguration transportConfiguration = (Lwm2mDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration(); + this.powerMode = transportConfiguration.getPowerMode(); + this.edrxCycle = transportConfiguration.getEdrxCycle(); } public void onDeviceProfileUpdate(DeviceProfile deviceProfile) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java index 643ab61737..65dec09fc8 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java @@ -62,4 +62,6 @@ public interface LwM2mClientContext { void sendMsgsAfterSleeping(LwM2mClient lwM2MClient); boolean isComposite(LwM2mClient client); + + Long getRequestTimeout(LwM2mClient client); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index b861ce4cd0..5b92323634 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -26,6 +26,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.device.data.PowerMode; +import org.thingsboard.server.common.data.device.data.lwm2m.OtherConfiguration; import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.transport.TransportDeviceProfileCache; @@ -346,6 +347,24 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { getProfile(client.getProfileId()).getClientLwM2mSettings().isCompositeOperationsSupport(); } + @Override + public Long getRequestTimeout(LwM2mClient client) { + Long timeout = null; + if (PowerMode.E_DRX.equals(client.getPowerMode()) && client.getEdrxCycle() != null) { + timeout = client.getEdrxCycle(); + } else { + var clientProfile = getProfile(client.getProfileId()); + OtherConfiguration clientLwM2mSettings = clientProfile.getClientLwM2mSettings(); + if (PowerMode.E_DRX.equals(clientLwM2mSettings.getPowerMode())) { + timeout = clientLwM2mSettings.getEdrxCycle(); + } + } + if (timeout == null || timeout == 0L) { + timeout = this.config.getTimeout(); + } + return timeout; + } + private boolean validateResourceInModel(LwM2mClient lwM2mClient, String pathIdVer, boolean isWritableNotOptional) { ResourceModel resourceModel = lwM2mClient.getResourceModel(pathIdVer, this.config .getModelProvider()); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java index 619cbdd7a0..96c93b177a 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java @@ -131,7 +131,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im ContentFormat responseContentFormat = ContentFormat.SENML_JSON; ReadCompositeRequest downlink = new ReadCompositeRequest(requestContentFormat, responseContentFormat, request.getObjectIds()); - sendCompositeRequest(client, downlink, this.config.getTimeout(), callback); + sendCompositeRequest(client, downlink, request.getTimeout(), callback); } @Override @@ -248,7 +248,8 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im ContentFormat contentFormat = ContentFormat.SENML_JSON; try { WriteCompositeRequest downlink = new WriteCompositeRequest(contentFormat, rpcWriteCompositeRequest.getNodes()); - sendWriteCompositeRequest(client, downlink, this.config.getTimeout(), callback); + //TODO: replace config.getTimeout(); + sendWriteCompositeRequest(client, downlink, config.getTimeout(), callback); } catch (Exception e) { callback.onError(JacksonUtil.toString(rpcWriteCompositeRequest), e); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java index 3074061b5b..ae724cb86c 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java @@ -417,7 +417,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl private void startUpdateUsingUrl(LwM2mClient client, String id, String url) { String targetIdVer = convertObjectIdToVersionedId(id, client.getRegistration()); - TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(targetIdVer).value(url).timeout(config.getTimeout()).build(); + TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(targetIdVer).value(url).timeout(clientContext.getRequestTimeout(client)).build(); downlinkHandler.sendWriteReplaceRequest(client, request, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, client, targetIdVer)); } @@ -486,7 +486,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl byte[] firmwareChunk = otaPackageDataCache.get(otaPackageId.toString(), 0, 0); TbLwM2MWriteReplaceRequest writeRequest = TbLwM2MWriteReplaceRequest.builder().versionedId(versionedId) .value(firmwareChunk).contentFormat(ContentFormat.OPAQUE) - .timeout(config.getTimeout()).build(); + .timeout(clientContext.getRequestTimeout(client)).build(); downlinkHandler.sendWriteReplaceRequest(client, writeRequest, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, client, versionedId)); } @@ -501,17 +501,17 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl } private void executeFwUpdate(LwM2mClient client) { - TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(FW_EXECUTE_ID).timeout(config.getTimeout()).build(); + TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(FW_EXECUTE_ID).timeout(clientContext.getRequestTimeout(client)).build(); downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, FW_EXECUTE_ID)); } private void executeSwInstall(LwM2mClient client) { - TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(SW_INSTALL_ID).timeout(config.getTimeout()).build(); + TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(SW_INSTALL_ID).timeout(clientContext.getRequestTimeout(client)).build(); downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, SW_INSTALL_ID)); } private void executeSwUninstallForUpdate(LwM2mClient client) { - TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(SW_UN_INSTALL_ID).params("1").timeout(config.getTimeout()).build(); + TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(SW_UN_INSTALL_ID).params("1").timeout(clientContext.getRequestTimeout(client)).build(); downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, SW_INSTALL_ID)); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java index 7954cdda98..0342d3dd05 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java @@ -171,7 +171,7 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { } private void sendReadRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) { - TbLwM2MReadRequest request = TbLwM2MReadRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build(); + TbLwM2MReadRequest request = TbLwM2MReadRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build(); var mainCallback = new TbLwM2MReadCallback(uplinkHandler, logService, client, versionedId); var rpcCallback = new RpcReadResponseCallback<>(transportService, client, requestMsg, mainCallback); downlinkHandler.sendReadRequest(client, request, rpcCallback); @@ -179,38 +179,38 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { private void sendReadCompositeRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) { String[] versionedIds = getIdsFromParameters(client, requestMsg); - TbLwM2MReadCompositeRequest request = TbLwM2MReadCompositeRequest.builder().versionedIds(versionedIds).timeout(this.config.getTimeout()).build(); + TbLwM2MReadCompositeRequest request = TbLwM2MReadCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(client)).build(); var mainCallback = new TbLwM2MReadCompositeCallback(uplinkHandler, logService, client, versionedIds); var rpcCallback = new RpcReadResponseCompositeCallback(transportService, client, requestMsg, mainCallback); downlinkHandler.sendReadCompositeRequest(client, request, rpcCallback); } private void sendObserveRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) { - TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build(); + TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build(); var mainCallback = new TbLwM2MObserveCallback(uplinkHandler, logService, client, versionedId); var rpcCallback = new RpcReadResponseCallback<>(transportService, client, requestMsg, mainCallback); downlinkHandler.sendObserveRequest(client, request, rpcCallback); } private void sendObserveAllRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) { - TbLwM2MObserveAllRequest request = TbLwM2MObserveAllRequest.builder().timeout(this.config.getTimeout()).build(); + TbLwM2MObserveAllRequest request = TbLwM2MObserveAllRequest.builder().timeout(clientContext.getRequestTimeout(client)).build(); downlinkHandler.sendObserveAllRequest(client, request, new RpcLinkSetCallback<>(transportService, client, requestMsg, null)); } private void sendDiscoverAllRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) { - TbLwM2MDiscoverAllRequest request = TbLwM2MDiscoverAllRequest.builder().timeout(this.config.getTimeout()).build(); + TbLwM2MDiscoverAllRequest request = TbLwM2MDiscoverAllRequest.builder().timeout(clientContext.getRequestTimeout(client)).build(); downlinkHandler.sendDiscoverAllRequest(client, request, new RpcLinkSetCallback<>(transportService, client, requestMsg, null)); } private void sendDiscoverRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) { - TbLwM2MDiscoverRequest request = TbLwM2MDiscoverRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build(); + TbLwM2MDiscoverRequest request = TbLwM2MDiscoverRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build(); var mainCallback = new TbLwM2MDiscoverCallback(logService, client, versionedId); var rpcCallback = new RpcDiscoverCallback(transportService, client, requestMsg, mainCallback); downlinkHandler.sendDiscoverRequest(client, request, rpcCallback); } private void sendExecuteRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) { - TbLwM2MExecuteRequest downlink = TbLwM2MExecuteRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build(); + TbLwM2MExecuteRequest downlink = TbLwM2MExecuteRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build(); var mainCallback = new TbLwM2MExecuteCallback(logService, client, versionedId); var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback); downlinkHandler.sendExecuteRequest(client, downlink, rpcCallback); @@ -220,7 +220,7 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { RpcWriteAttributesRequest requestBody = JacksonUtil.fromString(requestMsg.getParams(), RpcWriteAttributesRequest.class); TbLwM2MWriteAttributesRequest request = TbLwM2MWriteAttributesRequest.builder().versionedId(versionedId) .attributes(requestBody.getAttributes()) - .timeout(this.config.getTimeout()).build(); + .timeout(clientContext.getRequestTimeout(client)).build(); var mainCallback = new TbLwM2MWriteAttributesCallback(logService, client, versionedId); var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback); downlinkHandler.sendWriteAttributesRequest(client, request, rpcCallback); @@ -229,7 +229,7 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { private void sendWriteUpdateRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) { RpcWriteUpdateRequest requestBody = JacksonUtil.fromString(requestMsg.getParams(), RpcWriteUpdateRequest.class); TbLwM2MWriteUpdateRequest.TbLwM2MWriteUpdateRequestBuilder builder = TbLwM2MWriteUpdateRequest.builder().versionedId(versionedId); - builder.value(requestBody.getValue()).timeout(this.config.getTimeout()); + builder.value(requestBody.getValue()).timeout(clientContext.getRequestTimeout(client)); var mainCallback = new TbLwM2MWriteResponseCallback(uplinkHandler, logService, client, versionedId); var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback); downlinkHandler.sendWriteUpdateRequest(client, builder.build(), rpcCallback); @@ -239,7 +239,7 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { RpcWriteReplaceRequest requestBody = JacksonUtil.fromString(requestMsg.getParams(), RpcWriteReplaceRequest.class); TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(versionedId) .value(requestBody.getValue()) - .timeout(this.config.getTimeout()).build(); + .timeout(clientContext.getRequestTimeout(client)).build(); var mainCallback = new TbLwM2MWriteResponseCallback(uplinkHandler, logService, client, versionedId); var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback); downlinkHandler.sendWriteReplaceRequest(client, request, rpcCallback); @@ -261,21 +261,21 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { } private void sendCancelObserveRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) { - TbLwM2MCancelObserveRequest downlink = TbLwM2MCancelObserveRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build(); + TbLwM2MCancelObserveRequest downlink = TbLwM2MCancelObserveRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build(); var mainCallback = new TbLwM2MCancelObserveCallback(logService, client, versionedId); var rpcCallback = new RpcCancelObserveCallback(transportService, client, requestMsg, mainCallback); downlinkHandler.sendCancelObserveRequest(client, downlink, rpcCallback); } private void sendDeleteRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) { - TbLwM2MDeleteRequest downlink = TbLwM2MDeleteRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build(); + TbLwM2MDeleteRequest downlink = TbLwM2MDeleteRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build(); var mainCallback = new TbLwM2MDeleteCallback(logService, client, versionedId); var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback); downlinkHandler.sendDeleteRequest(client, downlink, rpcCallback); } private void sendCancelAllObserveRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) { - TbLwM2MCancelAllRequest downlink = TbLwM2MCancelAllRequest.builder().timeout(this.config.getTimeout()).build(); + TbLwM2MCancelAllRequest downlink = TbLwM2MCancelAllRequest.builder().timeout(clientContext.getRequestTimeout(client)).build(); var mainCallback = new TbLwM2MCancelAllObserveCallback(logService, client); var rpcCallback = new RpcCancelAllObserveCallback(transportService, client, requestMsg, mainCallback); downlinkHandler.sendCancelAllRequest(client, downlink, rpcCallback); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2MUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2MUplinkMsgHandler.java index 8f8e4f307e..257fff72eb 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2MUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2MUplinkMsgHandler.java @@ -457,7 +457,7 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl } private void sendDiscoverRequest(LwM2mClient lwM2MClient, String targetId) { - TbLwM2MDiscoverRequest request = TbLwM2MDiscoverRequest.builder().versionedId(targetId).timeout(this.config.getTimeout()).build(); + TbLwM2MDiscoverRequest request = TbLwM2MDiscoverRequest.builder().versionedId(targetId).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); defaultLwM2MDownlinkMsgHandler.sendDiscoverRequest(lwM2MClient, request, new TbLwM2MDiscoverCallback(logService, lwM2MClient, targetId)); } @@ -466,7 +466,7 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl } private void sendReadRequest(LwM2mClient lwM2MClient, String versionedId, DownlinkRequestCallback callback) { - TbLwM2MReadRequest request = TbLwM2MReadRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build(); + TbLwM2MReadRequest request = TbLwM2MReadRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); defaultLwM2MDownlinkMsgHandler.sendReadRequest(lwM2MClient, request, callback); } @@ -475,17 +475,17 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl } private void sendObserveRequest(LwM2mClient lwM2MClient, String versionedId, DownlinkRequestCallback callback) { - TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build(); + TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); defaultLwM2MDownlinkMsgHandler.sendObserveRequest(lwM2MClient, request, callback); } private void sendWriteAttributesRequest(LwM2mClient lwM2MClient, String targetId, ObjectAttributes params) { - TbLwM2MWriteAttributesRequest request = TbLwM2MWriteAttributesRequest.builder().versionedId(targetId).attributes(params).timeout(this.config.getTimeout()).build(); + TbLwM2MWriteAttributesRequest request = TbLwM2MWriteAttributesRequest.builder().versionedId(targetId).attributes(params).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); defaultLwM2MDownlinkMsgHandler.sendWriteAttributesRequest(lwM2MClient, request, new TbLwM2MWriteAttributesCallback(logService, lwM2MClient, targetId)); } private void sendCancelObserveRequest(String versionedId, LwM2mClient client) { - TbLwM2MCancelObserveRequest request = TbLwM2MCancelObserveRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build(); + TbLwM2MCancelObserveRequest request = TbLwM2MCancelObserveRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build(); defaultLwM2MDownlinkMsgHandler.sendCancelObserveRequest(client, request, new TbLwM2MCancelObserveCallback(logService, client, versionedId)); } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 3d073298de..23f3a9cc5f 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -17,7 +17,6 @@ package org.thingsboard.server.transport.mqtt; import com.fasterxml.jackson.databind.JsonNode; import com.google.gson.JsonParseException; -import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.mqtt.MqttConnAckMessage; @@ -40,6 +39,7 @@ import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; +import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.DataConstants; @@ -129,6 +129,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private final ConcurrentHashMap otaPackSessions; private final ConcurrentHashMap chunkSizes; + private final ConcurrentMap rpcAwaitingAck; MqttTransportHandler(MqttTransportContext context, SslHandler sslHandler) { this.sessionId = UUID.randomUUID(); @@ -140,6 +141,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement this.deviceSessionCtx = new DeviceSessionCtx(sessionId, mqttQoSMap, context); this.otaPackSessions = new ConcurrentHashMap<>(); this.chunkSizes = new ConcurrentHashMap<>(); + this.rpcAwaitingAck = new ConcurrentHashMap<>(); } @Override @@ -243,6 +245,13 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement processDisconnect(ctx); } break; + case PUBACK: + int msgId = ((MqttPubAckMessage) msg).variableHeader().messageId(); + TransportProtos.ToDeviceRpcRequestMsg rpcRequest = rpcAwaitingAck.remove(msgId); + if (rpcRequest != null) { + transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, false, TransportServiceCallback.EMPTY); + } + break; default: break; } @@ -815,16 +824,22 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { log.trace("[{}] Received RPC command to device", sessionId); try { - deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, rpcRequest) - .ifPresent(payload -> { - ChannelFuture channelFuture = deviceSessionCtx.getChannel().writeAndFlush(payload); - if (rpcRequest.getPersisted()) { - channelFuture.addListener(future -> - transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, - future.cause() != null, TransportServiceCallback.EMPTY) - ); - } - }); + deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, rpcRequest).ifPresent(payload -> { + RequestInfo requestInfo = publish(payload, deviceSessionCtx); + int msgId = requestInfo.getMsgId(); + + if (isAckExpected(payload)) { + if (rpcRequest.getPersisted()) { + rpcAwaitingAck.put(msgId, rpcRequest); + context.getScheduler().schedule(() -> { + TransportProtos.ToDeviceRpcRequestMsg awaitingAckMsg = rpcAwaitingAck.remove(msgId); + if (awaitingAckMsg != null) { + transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, true, TransportServiceCallback.EMPTY); + } + }, Math.max(0, rpcRequest.getExpirationTime() - System.currentTimeMillis()), TimeUnit.MILLISECONDS); + } + } + }); } catch (Exception e) { log.trace("[{}] Failed to convert device RPC command to MQTT msg", sessionId, e); } @@ -840,6 +855,19 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + private RequestInfo publish(MqttMessage message, DeviceSessionCtx deviceSessionCtx) { + deviceSessionCtx.getChannel().writeAndFlush(message); + + int msgId = ((MqttPublishMessage) message).variableHeader().packetId(); + RequestInfo requestInfo = new RequestInfo(msgId, System.currentTimeMillis(), deviceSessionCtx.getSessionInfo()); + + return requestInfo; + } + + private boolean isAckExpected(MqttMessage message) { + return message.fixedHeader().qosLevel().value() > 0; + } + @Override public void onDeviceProfileUpdate(TransportProtos.SessionInfoProto sessionInfo, DeviceProfile deviceProfile) { deviceSessionCtx.onDeviceProfileUpdate(sessionInfo, deviceProfile); @@ -849,4 +877,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt) { deviceSessionCtx.onDeviceUpdate(sessionInfo, device, deviceProfileOpt); } + + @Data + public static class RequestInfo { + private final int msgId; + private final long requestTime; + private final TransportProtos.SessionInfoProto sessionInfo; + } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java index f6ef357a93..92bd389f60 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java @@ -35,5 +35,5 @@ public class TransportDeviceInfo implements Serializable { private String deviceType; private PowerMode powerMode; private String additionalInfo; - + private Long edrxCycle; } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index dba34957b9..1a46f61fe9 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -443,6 +443,7 @@ public class DefaultTransportService implements TransportService { tdi.setDeviceType(di.getDeviceType()); if (StringUtils.isNotEmpty(di.getPowerMode())) { tdi.setPowerMode(PowerMode.valueOf(di.getPowerMode())); + tdi.setEdrxCycle(di.getEdrxCycle()); } return tdi; } diff --git a/dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql b/dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql new file mode 100644 index 0000000000..5ea65a3380 --- /dev/null +++ b/dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql @@ -0,0 +1,38 @@ +-- +-- 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. +-- + +-- This file describes PostgreSQL-specific indexes that not supported by hsql +-- It is not a stand-alone file! Run schema-entities-idx.sql before! +-- Note: Hibernate DESC order translates to native SQL "ORDER BY .. DESC NULLS LAST" +-- While creating index PostgreSQL transforms short notation (ts DESC) to the full (DESC NULLS FIRST) +-- That difference between NULLS LAST and NULLS FIRST prevents to hit index while querying latest by ts +-- That why we need to define DESC index explicitly as (ts DESC NULLS LAST) + +CREATE INDEX IF NOT EXISTS idx_event_ts + ON public.event + (ts DESC NULLS LAST) + WITH (FILLFACTOR=95); + +COMMENT ON INDEX public.idx_event_ts + IS 'This index helps to delete events by TTL using timestamp'; + +CREATE INDEX IF NOT EXISTS idx_event_tenant_entity_type_entity_event_type_created_time_des + ON public.event + (tenant_id ASC, entity_type ASC, entity_id ASC, event_type ASC, created_time DESC NULLS LAST) + WITH (FILLFACTOR=95); + +COMMENT ON INDEX public.idx_event_tenant_entity_type_entity_event_type_created_time_des + IS 'This index helps to open latest events on UI fast'; diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index d649e44418..78b1eda905 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -22,8 +22,6 @@ CREATE INDEX IF NOT EXISTS idx_alarm_tenant_created_time ON alarm(tenant_id, cre CREATE INDEX IF NOT EXISTS idx_alarm_tenant_alarm_type_created_time ON alarm(tenant_id, type, created_time DESC); -CREATE INDEX IF NOT EXISTS idx_event_type_entity_id ON event(tenant_id, event_type, entity_type, entity_id); - CREATE INDEX IF NOT EXISTS idx_relation_to_id ON relation(relation_type_group, to_type, to_id); CREATE INDEX IF NOT EXISTS idx_relation_from_id ON relation(relation_type_group, from_type, from_id); diff --git a/dao/src/test/resources/sql-test.properties b/dao/src/test/resources/sql-test.properties index 358b9ea3af..e81637ac3e 100644 --- a/dao/src/test/resources/sql-test.properties +++ b/dao/src/test/resources/sql-test.properties @@ -15,7 +15,7 @@ spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect spring.datasource.username=sa spring.datasource.password= -spring.datasource.url=jdbc:hsqldb:file:target/tmp/testDb;sql.enforce_size=false +spring.datasource.url=jdbc:hsqldb:file:target/tmp/testDb;sql.enforce_size=false;sql.syntax_pgs=true spring.datasource.driverClassName=org.hsqldb.jdbc.JDBCDriver spring.datasource.hikari.maximumPoolSize = 50 diff --git a/docker/tb-coap-transport.env b/docker/tb-coap-transport.env index 77ec90c2db..9e6a41c930 100644 --- a/docker/tb-coap-transport.env +++ b/docker/tb-coap-transport.env @@ -9,4 +9,7 @@ METRICS_ENABLED=true METRICS_ENDPOINTS_EXPOSE=prometheus WEB_APPLICATION_ENABLE=true WEB_APPLICATION_TYPE=servlet -HTTP_BIND_PORT=8081 \ No newline at end of file +HTTP_BIND_PORT=8081 + +CACHE_TYPE=redis +REDIS_HOST=redis diff --git a/docker/tb-http-transport.env b/docker/tb-http-transport.env index ab49d9b326..1b4ce7a298 100644 --- a/docker/tb-http-transport.env +++ b/docker/tb-http-transport.env @@ -6,4 +6,7 @@ HTTP_BIND_PORT=8081 HTTP_REQUEST_TIMEOUT=60000 METRICS_ENABLED=true -METRICS_ENDPOINTS_EXPOSE=prometheus \ No newline at end of file +METRICS_ENDPOINTS_EXPOSE=prometheus + +CACHE_TYPE=redis +REDIS_HOST=redis diff --git a/docker/tb-lwm2m-transport.env b/docker/tb-lwm2m-transport.env index f284803a46..4616d45972 100644 --- a/docker/tb-lwm2m-transport.env +++ b/docker/tb-lwm2m-transport.env @@ -10,3 +10,6 @@ METRICS_ENDPOINTS_EXPOSE=prometheus WEB_APPLICATION_ENABLE=true WEB_APPLICATION_TYPE=servlet HTTP_BIND_PORT=8081 + +CACHE_TYPE=redis +REDIS_HOST=redis diff --git a/docker/tb-mqtt-transport.env b/docker/tb-mqtt-transport.env index a0927d9a2f..0cd51c7371 100644 --- a/docker/tb-mqtt-transport.env +++ b/docker/tb-mqtt-transport.env @@ -9,4 +9,7 @@ METRICS_ENABLED=true METRICS_ENDPOINTS_EXPOSE=prometheus WEB_APPLICATION_ENABLE=true WEB_APPLICATION_TYPE=servlet -HTTP_BIND_PORT=8081 \ No newline at end of file +HTTP_BIND_PORT=8081 + +CACHE_TYPE=redis +REDIS_HOST=redis diff --git a/docker/tb-snmp-transport.env b/docker/tb-snmp-transport.env index e2cc39d658..4851e9f6c1 100644 --- a/docker/tb-snmp-transport.env +++ b/docker/tb-snmp-transport.env @@ -6,3 +6,6 @@ METRICS_ENDPOINTS_EXPOSE=prometheus WEB_APPLICATION_ENABLE=true WEB_APPLICATION_TYPE=servlet HTTP_BIND_PORT=8081 + +CACHE_TYPE=redis +REDIS_HOST=redis diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index 68331569ab..5c6740e61d 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -35,7 +35,7 @@ UTF-8 ${basedir}/../.. true - 1.9.1 + 1.11.4 1.10 4.5.13 diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java index acaf417882..c5abbe864b 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.msa; +import lombok.extern.slf4j.Slf4j; import org.junit.ClassRule; import org.junit.extensions.cpsuite.ClasspathSuite; import org.junit.rules.ExternalResource; @@ -32,6 +33,7 @@ import java.util.Map; @RunWith(ClasspathSuite.class) @ClasspathSuite.ClassnameFilters({"org.thingsboard.server.msa.*Test"}) +@Slf4j public class ContainerTestSuite { private static DockerComposeContainer testContainer; @@ -43,17 +45,28 @@ public class ContainerTestSuite { public static DockerComposeContainer getTestContainer() { if (testContainer == null) { boolean skipTailChildContainers = Boolean.valueOf(System.getProperty("blackBoxTests.skipTailChildContainers")); - testContainer = new DockerComposeContainer<>( - new File("./../../docker/docker-compose.yml"), - new File("./../../docker/docker-compose.postgres.yml"), - new File("./../../docker/docker-compose.postgres.volumes.yml"), - new File("./../../docker/docker-compose.kafka.yml")) - .withPull(false) - .withLocalCompose(true) - .withTailChildContainers(!skipTailChildContainers) - .withEnv(installTb.getEnv()) - .withEnv("LOAD_BALANCER_NAME", "") - .withExposedService("haproxy", 80, Wait.forHttp("/swagger-ui.html").withStartupTimeout(Duration.ofSeconds(400))); + try { + String logRegexp = ".*Going to recalculate partitions.*"; + + testContainer = new DockerComposeContainer<>( + new File("./../../docker/docker-compose.yml"), + new File("./../../docker/docker-compose.postgres.yml"), + new File("./../../docker/docker-compose.postgres.volumes.yml"), + new File("./../../docker/docker-compose.kafka.yml")) + .withPull(false) + .withLocalCompose(true) + .withTailChildContainers(!skipTailChildContainers) + .withEnv(installTb.getEnv()) + .withEnv("LOAD_BALANCER_NAME", "") + .withExposedService("haproxy", 80, Wait.forHttp("/swagger-ui.html").withStartupTimeout(Duration.ofSeconds(400))) + .waitingFor("tb-http-transport1", Wait.forLogMessage(logRegexp, 1).withStartupTimeout(Duration.ofSeconds(400))) + .waitingFor("tb-http-transport2", Wait.forLogMessage(logRegexp, 1).withStartupTimeout(Duration.ofSeconds(400))) + .waitingFor("tb-mqtt-transport1", Wait.forLogMessage(logRegexp, 1).withStartupTimeout(Duration.ofSeconds(400))) + .waitingFor("tb-mqtt-transport2", Wait.forLogMessage(logRegexp, 1).withStartupTimeout(Duration.ofSeconds(400))); + } catch (Exception e) { + log.error("Failed to create test container", e); + throw e; + } } return testContainer; } diff --git a/pom.xml b/pom.xml index 5c64b56849..db120d4560 100755 --- a/pom.xml +++ b/pom.xml @@ -90,7 +90,7 @@ 1.15.0 1.64 2.0.1 - 2.5.0 + 2.6.0 2.5.3 1.2.1 42.2.20 @@ -99,7 +99,7 @@ 5.0.2 0.1.16 - 2.6.0 + 2.8.0 4.1.1 2.57 2.7.7 diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java index 0311a5b26f..8108dd13a6 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java @@ -46,9 +46,9 @@ public interface MailService { void sendAccountLockoutEmail(String lockoutEmail, String email, Integer maxFailedLoginAttempts) throws ThingsboardException; - void send(TenantId tenantId, CustomerId customerId, String from, String to, String cc, String bcc, String subject, String body, boolean isHtml, Map images) throws ThingsboardException; + void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail) throws ThingsboardException; - void send(TenantId tenantId, CustomerId customerId, String from, String to, String cc, String bcc, String subject, String body, boolean isHtml, Map images, JavaMailSender javaMailSender) throws ThingsboardException; + void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail, JavaMailSender javaMailSender) throws ThingsboardException; void sendApiFeatureStateEmail(ApiFeature apiFeature, ApiUsageStateValue stateValue, String email, ApiUsageStateMailMessage msg) throws ThingsboardException; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/EmailPojo.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbEmail.java similarity index 93% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/EmailPojo.java rename to rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbEmail.java index 5d43b47c3b..884fd0e46e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/EmailPojo.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbEmail.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.mail; +package org.thingsboard.rule.engine.api; import lombok.Builder; import lombok.Data; @@ -22,7 +22,7 @@ import java.util.Map; @Data @Builder -class EmailPojo { +public class TbEmail { private final String from; private final String to; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java index bc2372a2a6..ed84e6b581 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java @@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbEmail; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; @@ -65,7 +66,7 @@ public class TbMsgToEmailNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { try { - EmailPojo email = convert(msg); + TbEmail email = convert(msg); TbMsg emailMsg = buildEmailMsg(ctx, msg, email); ctx.tellNext(emailMsg, SUCCESS); } catch (Exception ex) { @@ -74,13 +75,13 @@ public class TbMsgToEmailNode implements TbNode { } } - private TbMsg buildEmailMsg(TbContext ctx, TbMsg msg, EmailPojo email) throws JsonProcessingException { + private TbMsg buildEmailMsg(TbContext ctx, TbMsg msg, TbEmail email) throws JsonProcessingException { String emailJson = MAPPER.writeValueAsString(email); return ctx.transformMsg(msg, SEND_EMAIL_TYPE, msg.getOriginator(), msg.getMetaData().copy(), emailJson); } - private EmailPojo convert(TbMsg msg) throws IOException { - EmailPojo.EmailPojoBuilder builder = EmailPojo.builder(); + private TbEmail convert(TbMsg msg) throws IOException { + TbEmail.TbEmailBuilder builder = TbEmail.builder(); builder.from(fromTemplate(this.config.getFromTemplate(), msg)); builder.to(fromTemplate(this.config.getToTemplate(), msg)); builder.cc(fromTemplate(this.config.getCcTemplate(), msg)); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java index 8d0a2f3275..4987a628d6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java @@ -21,6 +21,7 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbEmail; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; @@ -71,7 +72,7 @@ public class TbSendEmailNode implements TbNode { public void onMsg(TbContext ctx, TbMsg msg) { try { validateType(msg.getType()); - EmailPojo email = getEmail(msg); + TbEmail email = getEmail(msg); withCallback(ctx.getMailExecutor().executeAsync(() -> { sendEmail(ctx, msg, email); return null; @@ -83,18 +84,16 @@ public class TbSendEmailNode implements TbNode { } } - private void sendEmail(TbContext ctx, TbMsg msg, EmailPojo email) throws Exception { + private void sendEmail(TbContext ctx, TbMsg msg, TbEmail email) throws Exception { if (this.config.isUseSystemSmtpSettings()) { - ctx.getMailService().send(ctx.getTenantId(), msg.getCustomerId(), email.getFrom(), email.getTo(), email.getCc(), - email.getBcc(), email.getSubject(), email.getBody(), email.isHtml(), email.getImages()); + ctx.getMailService().send(ctx.getTenantId(), msg.getCustomerId(), email); } else { - ctx.getMailService().send(ctx.getTenantId(), msg.getCustomerId(), email.getFrom(), email.getTo(), email.getCc(), - email.getBcc(), email.getSubject(), email.getBody(), email.isHtml(), email.getImages(), this.mailSender); + ctx.getMailService().send(ctx.getTenantId(), msg.getCustomerId(), email, this.mailSender); } } - private EmailPojo getEmail(TbMsg msg) throws IOException { - EmailPojo email = MAPPER.readValue(msg.getData(), EmailPojo.class); + private TbEmail getEmail(TbMsg msg) throws IOException { + TbEmail email = MAPPER.readValue(msg.getData(), TbEmail.class); if (StringUtils.isBlank(email.getTo())) { throw new IllegalStateException("Email destination can not be blank [" + email.getTo() + "]"); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java index 1867ae4805..3d66c569d8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java @@ -81,7 +81,7 @@ public class TbSendRPCRequestNode implements TbNode { tmp = msg.getMetaData().getValue("oneway"); boolean oneway = !StringUtils.isEmpty(tmp) && Boolean.parseBoolean(tmp); - tmp = msg.getMetaData().getValue("persisted"); + tmp = msg.getMetaData().getValue(DataConstants.PERSISTENT); boolean persisted = !StringUtils.isEmpty(tmp) && Boolean.parseBoolean(tmp); tmp = msg.getMetaData().getValue("requestUUID"); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java index c29e45f569..b5b8574e2f 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java @@ -23,6 +23,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbEmail; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.DeviceId; @@ -79,9 +80,9 @@ public class TbMsgToEmailNodeTest { assertEquals("oreo", metadataCaptor.getValue().getValue("username")); assertNotSame(metaData, metadataCaptor.getValue()); - EmailPojo actual = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), EmailPojo.class); + TbEmail actual = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), TbEmail.class); - EmailPojo expected = new EmailPojo.EmailPojoBuilder() + TbEmail expected = TbEmail.builder() .from("test@mail.org") .to("user@email.io") .subject("Hi oreo there") diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index 1b4a68a906..331e7482be 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -45,10 +45,10 @@ - - - - + + org.springframework.boot + spring-boot-starter-web + org.thingsboard.common.transport lwm2m diff --git a/ui-ngx/src/app/app-routing.module.ts b/ui-ngx/src/app/app-routing.module.ts index f11b8b6f82..a7b68f5cad 100644 --- a/ui-ngx/src/app/app-routing.module.ts +++ b/ui-ngx/src/app/app-routing.module.ts @@ -30,7 +30,7 @@ const routes: Routes = [ ]; @NgModule({ - imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })], + imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } 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 515e5397f1..d473296a4c 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -69,8 +69,8 @@ export interface WidgetSubscriptionApi { } export interface RpcApi { - sendOneWayCommand: (method: string, params?: any, timeout?: number, requestUUID?: string) => Observable; - sendTwoWayCommand: (method: string, params?: any, timeout?: number, requestUUID?: string) => Observable; + sendOneWayCommand: (method: string, params?: any, timeout?: number, persistent?: boolean, requestUUID?: string) => Observable; + sendTwoWayCommand: (method: string, params?: any, timeout?: number, persistent?: boolean, requestUUID?: string) => Observable; } export interface IWidgetUtils { @@ -299,8 +299,8 @@ export interface IWidgetSubscription { onResetTimewindow(): void; updateTimewindowConfig(newTimewindow: Timewindow): void; - sendOneWayCommand(method: string, params?: any, timeout?: number, requestUUID?: string): Observable; - sendTwoWayCommand(method: string, params?: any, timeout?: number, requestUUID?: string): Observable; + sendOneWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, requestUUID?: string): Observable; + sendTwoWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, requestUUID?: string): Observable; clearRpcError(): void; subscribe(): void; diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 7e0c9ad620..32f0f90005 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -644,12 +644,12 @@ export class WidgetSubscription implements IWidgetSubscription { } } - sendOneWayCommand(method: string, params?: any, timeout?: number, requestUUID?: string): Observable { - return this.sendCommand(true, method, params, timeout, requestUUID); + sendOneWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, requestUUID?: string): Observable { + return this.sendCommand(true, method, params, timeout, persistent, requestUUID); } - sendTwoWayCommand(method: string, params?: any, timeout?: number, requestUUID?: string): Observable { - return this.sendCommand(false, method, params, timeout, requestUUID); + sendTwoWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, requestUUID?: string): Observable { + return this.sendCommand(false, method, params, timeout, persistent, requestUUID); } clearRpcError(): void { @@ -658,7 +658,8 @@ export class WidgetSubscription implements IWidgetSubscription { this.callbacks.onRpcErrorCleared(this); } - sendCommand(oneWayElseTwoWay: boolean, method: string, params?: any, timeout?: number, requestUUID?: string): Observable { + sendCommand(oneWayElseTwoWay: boolean, method: string, params?: any, + timeout?: number, persistent?: boolean, requestUUID?: string): Observable { if (!this.rpcEnabled) { return throwError(new Error('Rpc disabled!')); } else { @@ -670,6 +671,7 @@ export class WidgetSubscription implements IWidgetSubscription { const requestBody: any = { method, params, + persistent, requestUUID }; if (timeout && timeout > 0) { diff --git a/ui-ngx/src/app/core/http/attribute.service.ts b/ui-ngx/src/app/core/http/attribute.service.ts index 9df035d8b4..1bed864ab1 100644 --- a/ui-ngx/src/app/core/http/attribute.service.ts +++ b/ui-ngx/src/app/core/http/attribute.service.ts @@ -135,4 +135,13 @@ export class AttributeService { return this.http.get(url, defaultHttpOptionsFromConfig(config)); } + + public getEntityTimeseriesLatest(entityId: EntityId, keys?: Array, + useStrictDataTypes = false, config?: RequestConfig): Observable { + let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries?useStrictDataTypes=${useStrictDataTypes}`; + if (isDefinedAndNotNull(keys) && keys.length) { + url += `&keys=${keys.join(',')}`; + } + return this.http.get(url, defaultHttpOptionsFromConfig(config)); + } } diff --git a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m-server.component.html b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/device/security-config-lwm2m-server.component.html rename to ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.html diff --git a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m-server.component.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.ts similarity index 93% rename from ui-ngx/src/app/modules/home/components/device/security-config-lwm2m-server.component.ts rename to ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.ts index 09b7014212..267650eef7 100644 --- a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m-server.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.ts @@ -39,24 +39,24 @@ import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; @Component({ - selector: 'tb-security-config-lwm2m-server', - templateUrl: './security-config-lwm2m-server.component.html', + selector: 'tb-device-credentials-lwm2m-server', + templateUrl: './device-credentials-lwm2m-server.component.html', styleUrls: [], providers: [ { provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SecurityConfigLwm2mServerComponent), + useExisting: forwardRef(() => DeviceCredentialsLwm2mServerComponent), multi: true }, { provide: NG_VALIDATORS, - useExisting: forwardRef(() => SecurityConfigLwm2mServerComponent), + useExisting: forwardRef(() => DeviceCredentialsLwm2mServerComponent), multi: true } ] }) -export class SecurityConfigLwm2mServerComponent implements OnDestroy, ControlValueAccessor, Validator { +export class DeviceCredentialsLwm2mServerComponent implements OnDestroy, ControlValueAccessor, Validator { serverFormGroup: FormGroup; securityConfigLwM2MType = Lwm2mSecurityType; diff --git a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.html b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html similarity index 88% rename from ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.html rename to ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html index 77b7601ed5..c41b4b6900 100644 --- a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.html +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html @@ -15,8 +15,8 @@ limitations under the License. --> - - + + device.lwm2m-security-config.endpoint @@ -79,7 +79,7 @@ - +
@@ -89,9 +89,9 @@ - - + @@ -101,23 +101,12 @@ - - +
- - - - - -
diff --git a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.scss b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.scss rename to ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.scss diff --git a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts similarity index 89% rename from ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.ts rename to ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts index eb920d6190..a118de24d6 100644 --- a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts @@ -40,24 +40,24 @@ import { takeUntil } from 'rxjs/operators'; import { isDefinedAndNotNull } from '@core/utils'; @Component({ - selector: 'tb-security-config-lwm2m', - templateUrl: './security-config-lwm2m.component.html', - styleUrls: ['./security-config-lwm2m.component.scss'], + selector: 'tb-device-credentials-lwm2m', + templateUrl: './device-credentials-lwm2m.component.html', + styleUrls: ['./device-credentials-lwm2m.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => SecurityConfigLwm2mComponent), + useExisting: forwardRef(() => DeviceCredentialsLwm2mComponent), multi: true }, { provide: NG_VALIDATORS, - useExisting: forwardRef(() => SecurityConfigLwm2mComponent), + useExisting: forwardRef(() => DeviceCredentialsLwm2mComponent), multi: true } ] }) -export class SecurityConfigLwm2mComponent implements ControlValueAccessor, Validator, OnDestroy { +export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Validator, OnDestroy { lwm2mConfigFormGroup: FormGroup; securityConfigLwM2MType = Lwm2mSecurityType; @@ -89,6 +89,7 @@ export class SecurityConfigLwm2mComponent implements ControlValueAccessor, Valid this.lwm2mConfigFormGroup.disable({emitEvent: false}); } else { this.lwm2mConfigFormGroup.enable({emitEvent: false}); + this.securityConfigClientUpdateValidators(this.lwm2mConfigFormGroup.get('client.securityConfigClientMode').value); } } @@ -126,21 +127,21 @@ export class SecurityConfigLwm2mComponent implements ControlValueAccessor, Valid switch (mode) { case Lwm2mSecurityType.NO_SEC: this.setValidatorsNoSecX509(); - this.lwm2mConfigFormGroup.get('client.cert').disable(); + this.lwm2mConfigFormGroup.get('client.cert').disable({emitEvent: false}); break; case Lwm2mSecurityType.X509: this.setValidatorsNoSecX509(); - this.lwm2mConfigFormGroup.get('client.cert').enable(); + this.lwm2mConfigFormGroup.get('client.cert').enable({emitEvent: false}); break; case Lwm2mSecurityType.PSK: this.lenMaxKeyClient = LEN_MAX_PSK; this.setValidatorsPskRpk(mode); - this.lwm2mConfigFormGroup.get('client.identity').enable(); + this.lwm2mConfigFormGroup.get('client.identity').enable({emitEvent: false}); break; case Lwm2mSecurityType.RPK: this.lenMaxKeyClient = LEN_MAX_PUBLIC_KEY_RPK; this.setValidatorsPskRpk(mode); - this.lwm2mConfigFormGroup.get('client.identity').disable(); + this.lwm2mConfigFormGroup.get('client.identity').disable({emitEvent: false}); break; } this.lwm2mConfigFormGroup.get('client.identity').updateValueAndValidity({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html b/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html new file mode 100644 index 0000000000..0b798e6dbf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html @@ -0,0 +1,48 @@ + +
+ + device.client-id + + + {{ 'device.client-id-pattern' | translate }} + + + + device.user-name + + + {{ 'device.user-name-required' | translate }} + + + + device.password + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.ts new file mode 100644 index 0000000000..d4c88431a9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.ts @@ -0,0 +1,133 @@ +/// +/// 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 { Component, forwardRef, Input, OnDestroy } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + ValidatorFn, + Validators +} from '@angular/forms'; +import { Subject } from 'rxjs'; +import { DeviceCredentialMQTTBasic } from '@shared/models/device.models'; +import { takeUntil } from 'rxjs/operators'; +import { isDefinedAndNotNull, isEmptyStr } from '@core/utils'; + +@Component({ + selector: 'tb-device-credentials-mqtt-basic', + templateUrl: './device-credentials-mqtt-basic.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceCredentialsMqttBasicComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceCredentialsMqttBasicComponent), + multi: true, + }], + styleUrls: [] +}) +export class DeviceCredentialsMqttBasicComponent implements ControlValueAccessor, Validator, OnDestroy { + + @Input() + disabled: boolean; + + deviceCredentialsMqttFormGroup: FormGroup; + + hidePassword = true; + + private destroy$ = new Subject(); + private propagateChange = (v: any) => {}; + + constructor(public fb: FormBuilder) { + this.deviceCredentialsMqttFormGroup = this.fb.group({ + clientId: [null, [Validators.pattern(/^[A-Za-z0-9]+$/)]], + userName: [null], + password: [null] + }, {validators: this.atLeastOne(Validators.required, ['clientId', 'userName'])}); + this.deviceCredentialsMqttFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + this.updateView(value); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void {} + + setDisabledState(isDisabled: boolean) { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceCredentialsMqttFormGroup.disable({emitEvent: false}); + } else { + this.deviceCredentialsMqttFormGroup.enable({emitEvent: false}); + } + } + + validate(): ValidationErrors | null { + return this.deviceCredentialsMqttFormGroup.valid ? null : { + deviceCredentialsMqttBasic: false + }; + } + + writeValue(mqttBasic: string) { + if (isDefinedAndNotNull(mqttBasic) && !isEmptyStr(mqttBasic)) { + const value = JSON.parse(mqttBasic); + this.deviceCredentialsMqttFormGroup.patchValue(value, {emitEvent: false}); + } + } + + updateView(value: DeviceCredentialMQTTBasic) { + const formValue = JSON.stringify(value); + this.propagateChange(formValue); + } + + passwordChanged() { + const value = this.deviceCredentialsMqttFormGroup.get('password').value; + if (value !== '') { + this.deviceCredentialsMqttFormGroup.get('userName').setValidators([Validators.required]); + } else { + this.deviceCredentialsMqttFormGroup.get('userName').setValidators([]); + } + this.deviceCredentialsMqttFormGroup.get('userName').updateValueAndValidity({emitEvent: false}); + } + + private atLeastOne(validator: ValidatorFn, controls: string[] = null) { + return (group: FormGroup): ValidationErrors | null => { + if (!controls) { + controls = Object.keys(group.controls); + } + const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k])); + + return hasAtLeastOne ? null : {atLeastOne: true}; + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html index 0cf349a074..d5a846189d 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html @@ -16,7 +16,7 @@ -->
- + device.credentials-type @@ -24,58 +24,35 @@ - - device.access-token - - - {{ 'device.access-token-required' | translate }} - - - {{ 'device.access-token-invalid' | translate }} - - - - device.rsa-key - - - {{ 'device.rsa-key-required' | translate }} - - -
- - device.client-id - - - {{ 'device.client-id-pattern' | translate }} - - - - device.user-name - - - {{ 'device.user-name-required' | translate }} - - - - device.password - - - - -
-
- - +
+ + + device.access-token + + + {{ 'device.access-token-required' | translate }} + + + {{ 'device.access-token-invalid' | translate }} + + + + + + device.rsa-key + + + {{ 'device.rsa-key-required' | translate }} + + + + + + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts index 21c8d40a42..3617ecbcf4 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts @@ -22,16 +22,15 @@ import { FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, - ValidationErrors, Validator, - ValidatorFn, Validators } from '@angular/forms'; import { credentialTypeNames, - DeviceCredentialMQTTBasic, + credentialTypesByTransportType, DeviceCredentials, - DeviceCredentialsType + DeviceCredentialsType, + DeviceTransportType } from '@shared/models/device.models'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -58,32 +57,40 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, @Input() disabled: boolean; + private deviceTransportTypeValue = DeviceTransportType.DEFAULT; + get deviceTransportType(): DeviceTransportType { + return this.deviceTransportTypeValue; + } + @Input() + set deviceTransportType(type: DeviceTransportType) { + if (type) { + this.deviceTransportTypeValue = type; + this.credentialsTypes = credentialTypesByTransportType.get(type); + const currentType = this.deviceCredentialsFormGroup.get('credentialsType').value; + if (!this.credentialsTypes.includes(currentType)) { + this.deviceCredentialsFormGroup.get('credentialsType').patchValue(this.credentialsTypes[0], {onlySelf: true}); + } + } + } + private destroy$ = new Subject(); deviceCredentialsFormGroup: FormGroup; deviceCredentialsType = DeviceCredentialsType; - credentialsTypes = Object.values(DeviceCredentialsType); + credentialsTypes = credentialTypesByTransportType.get(DeviceTransportType.DEFAULT); credentialTypeNamesMap = credentialTypeNames; - hidePassword = true; - private propagateChange = (v: any) => {}; constructor(public fb: FormBuilder) { this.deviceCredentialsFormGroup = this.fb.group({ credentialsType: [DeviceCredentialsType.ACCESS_TOKEN], credentialsId: [null], - credentialsValue: [null], - credentialsBasic: this.fb.group({ - clientId: [null, [Validators.pattern(/^[A-Za-z0-9]+$/)]], - userName: [null], - password: [null] - }, {validators: this.atLeastOne(Validators.required, ['clientId', 'userName'])}) + credentialsValue: [null] }); - this.deviceCredentialsFormGroup.get('credentialsBasic').disable(); this.deviceCredentialsFormGroup.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(() => { @@ -109,18 +116,11 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, writeValue(value: DeviceCredentials | null): void { if (isDefinedAndNotNull(value)) { - let credentialsBasic = {clientId: null, userName: null, password: null}; - let credentialsValue = null; - if (value.credentialsType === DeviceCredentialsType.MQTT_BASIC) { - credentialsBasic = JSON.parse(value.credentialsValue) as DeviceCredentialMQTTBasic; - } else { - credentialsValue = value.credentialsValue; - } + const credentialsType = this.credentialsTypes.includes(value.credentialsType) ? value.credentialsType : this.credentialsTypes[0]; this.deviceCredentialsFormGroup.patchValue({ - credentialsType: value.credentialsType, + credentialsType, credentialsId: value.credentialsId, - credentialsValue, - credentialsBasic + credentialsValue: value.credentialsValue }, {emitEvent: false}); this.updateValidators(); } @@ -128,10 +128,6 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, updateView() { const deviceCredentialsValue = this.deviceCredentialsFormGroup.value; - if (deviceCredentialsValue.credentialsType === DeviceCredentialsType.MQTT_BASIC) { - deviceCredentialsValue.credentialsValue = JSON.stringify(deviceCredentialsValue.credentialsBasic); - } - delete deviceCredentialsValue.credentialsBasic; this.propagateChange(deviceCredentialsValue); } @@ -163,14 +159,12 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, credentialsTypeChanged(): void { this.deviceCredentialsFormGroup.patchValue({ credentialsId: null, - credentialsValue: null, - credentialsBasic: {clientId: '', userName: '', password: ''} + credentialsValue: null }); this.updateValidators(); } updateValidators(): void { - this.hidePassword = true; const credentialsType = this.deviceCredentialsFormGroup.get('credentialsType').value as DeviceCredentialsType; switch (credentialsType) { case DeviceCredentialsType.ACCESS_TOKEN: @@ -178,48 +172,13 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false}); this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]); this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false}); - this.deviceCredentialsFormGroup.get('credentialsBasic').disable({emitEvent: false}); break; - case DeviceCredentialsType.X509_CERTIFICATE: - case DeviceCredentialsType.LWM2M_CREDENTIALS: + default: this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([Validators.required]); this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false}); this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]); this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false}); - this.deviceCredentialsFormGroup.get('credentialsBasic').disable({emitEvent: false}); - break; - case DeviceCredentialsType.MQTT_BASIC: - this.deviceCredentialsFormGroup.get('credentialsBasic').enable({emitEvent: false}); - this.deviceCredentialsFormGroup.get('credentialsBasic').updateValueAndValidity({emitEvent: false}); - this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]); - this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false}); - this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]); - this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false}); break; } } - - private atLeastOne(validator: ValidatorFn, controls: string[] = null) { - return (group: FormGroup): ValidationErrors | null => { - if (!controls) { - controls = Object.keys(group.controls); - } - const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k])); - - return hasAtLeastOne ? null : {atLeastOne: true}; - }; - } - - passwordChanged() { - const value = this.deviceCredentialsFormGroup.get('credentialsBasic.password').value; - if (value !== '') { - this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([Validators.required]); - } else { - this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([]); - } - this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity({ - emitEvent: false, - onlySelf: true - }); - } } diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts new file mode 100644 index 0000000000..49f1c2c097 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts @@ -0,0 +1,46 @@ +/// +/// 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { CopyDeviceCredentialsComponent } from './copy-device-credentials.component'; +import { DeviceCredentialsComponent } from './device-credentials.component'; +import { DeviceCredentialsLwm2mComponent } from './device-credentials-lwm2m.component'; +import { DeviceCredentialsLwm2mServerComponent } from './device-credentials-lwm2m-server.component'; +import { DeviceCredentialsMqttBasicComponent } from './device-credentials-mqtt-basic.component'; + +@NgModule({ + declarations: [ + CopyDeviceCredentialsComponent, + DeviceCredentialsComponent, + DeviceCredentialsLwm2mComponent, + DeviceCredentialsLwm2mServerComponent, + DeviceCredentialsMqttBasicComponent + ], + imports: [ + CommonModule, + SharedModule + ], + exports: [ + CopyDeviceCredentialsComponent, + DeviceCredentialsComponent, + DeviceCredentialsLwm2mComponent, + DeviceCredentialsLwm2mServerComponent, + DeviceCredentialsMqttBasicComponent + ] +}) +export class DeviceCredentialsModule { } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 1ea914f939..3cfb4c19dc 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -110,7 +110,6 @@ import { RuleChainAutocompleteComponent } from '@home/components/rule-chain/rule import { DeviceProfileProvisionConfigurationComponent } from '@home/components/profile/device-profile-provision-configuration.component'; import { AlarmScheduleComponent } from '@home/components/profile/alarm/alarm-schedule.component'; import { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component'; -import { DeviceCredentialsComponent } from '@home/components/device/device-credentials.component'; import { AlarmScheduleInfoComponent } from '@home/components/profile/alarm/alarm-schedule-info.component'; import { AlarmScheduleDialogComponent } from '@home/components/profile/alarm/alarm-schedule-dialog.component'; import { EditAlarmDetailsDialogComponent } from '@home/components/profile/alarm/edit-alarm-details-dialog.component'; @@ -120,7 +119,6 @@ import { TenantProfileConfigurationComponent } from '@home/components/profile/te import { SmsProviderConfigurationComponent } from '@home/components/sms/sms-provider-configuration.component'; import { AwsSnsProviderConfigurationComponent } from '@home/components/sms/aws-sns-provider-configuration.component'; import { TwilioSmsProviderConfigurationComponent } from '@home/components/sms/twilio-sms-provider-configuration.component'; -import { CopyDeviceCredentialsComponent } from '@home/components/device/copy-device-credentials.component'; import { Lwm2mProfileComponentsModule } from '@home/components/profile/device/lwm2m/lwm2m-profile-components.module'; import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component'; import { DashboardToolbarComponent } from '@home/components/dashboard-page/dashboard-toolbar.component'; @@ -139,11 +137,10 @@ import { EdgeDownlinkTableComponent } from '@home/components/edge/edge-downlink- import { EdgeDownlinkTableHeaderComponent } from '@home/components/edge/edge-downlink-table-header.component'; import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component'; import { AlarmDurationPredicateValueComponent } from '@home/components/profile/alarm/alarm-duration-predicate-value.component'; -import { SecurityConfigLwm2mComponent } from '@home/components/device/security-config-lwm2m.component'; -import { SecurityConfigLwm2mServerComponent } from '@home/components/device/security-config-lwm2m-server.component'; import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component'; import { WidgetContainerComponent } from '@home/components/widget/widget-container.component'; import { SnmpDeviceProfileTransportModule } from '@home/components/profile/device/snpm/snmp-device-profile-transport.module'; +import { DeviceCredentialsModule } from '@home/components/device/device-credentials.module'; @NgModule({ declarations: @@ -245,10 +242,6 @@ import { SnmpDeviceProfileTransportModule } from '@home/components/profile/devic AlarmScheduleComponent, AlarmDurationPredicateValueComponent, DeviceWizardDialogComponent, - DeviceCredentialsComponent, - CopyDeviceCredentialsComponent, - SecurityConfigLwm2mComponent, - SecurityConfigLwm2mServerComponent, AlarmScheduleDialogComponent, EditAlarmDetailsDialogComponent, SmsProviderConfigurationComponent, @@ -274,7 +267,8 @@ import { SnmpDeviceProfileTransportModule } from '@home/components/profile/devic SharedHomeComponentsModule, Lwm2mProfileComponentsModule, SnmpDeviceProfileTransportModule, - StatesControllerModule + StatesControllerModule, + DeviceCredentialsModule ], exports: [ EntitiesTableComponent, @@ -353,10 +347,6 @@ import { SnmpDeviceProfileTransportModule } from '@home/components/profile/devic AddDeviceProfileDialogComponent, RuleChainAutocompleteComponent, DeviceWizardDialogComponent, - DeviceCredentialsComponent, - CopyDeviceCredentialsComponent, - SecurityConfigLwm2mComponent, - SecurityConfigLwm2mServerComponent, AlarmScheduleInfoComponent, AlarmScheduleComponent, AlarmScheduleDialogComponent, diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html index c546cc8912..b91613955d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html @@ -19,6 +19,7 @@

device-profile.add

+
\n \n", "cardCss": ".card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 13px 13px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n\n.card .mat-button {\n text-transform: uppercase;\n}\n\n.card .mat-button-wrapper {\n pointer-events: none;\n}\n\n.card .action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-button {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 10px;\n }\n .card .mat-button {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-button {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} \n\n\n" }, - "title": "Email messages", + "title": "Alarm created", "dropShadow": true, "enableFullscreen": false, "widgetStyle": { @@ -799,9 +800,14 @@ "name": "email_messages_details", "icon": "insert_chart", "type": "openDashboardState", - "targetDashboardStateId": "email_messages", + "targetDashboardStateId": "alarms_created", "setEntityId": false, "stateEntityParamName": null, + "openInSeparateDialog": null, + "dialogTitle": null, + "dialogHideDashboardToolbar": true, + "dialogWidth": null, + "dialogHeight": null, "openRightLayout": false, "id": "946ba769-84ac-1507-6baa-94701de8967b" } @@ -816,164 +822,6 @@ "col": 0, "id": "a151ae60-0326-6116-d818-9070dda8e9c7" }, - "2503a391-5692-0614-85e6-f179c5ee0dc9": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "html_value_card", - "type": "latest", - "title": "New widget", - "sizeX": 7.5, - "sizeY": 3, - "config": { - "datasources": [ - { - "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, - "dataKeys": [ - { - "name": "smsApiState", - "type": "timeseries", - "label": "apiState", - "color": "#2196f3", - "settings": {}, - "_hash": 0.8830669138660703, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "smsLimit", - "type": "timeseries", - "label": "limit", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "smsCount", - "type": "timeseries", - "label": "count", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "smsApiState", - "type": "timeseries", - "label": "apiStateClass", - "color": "#ffc107", - "settings": {}, - "_hash": 0.8737107059960671, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value.toLowerCase() : '';" - }, - { - "name": "smsApiState", - "type": "timeseries", - "label": "cardId", - "color": "#607d8b", - "settings": {}, - "_hash": 0.051659774305067296, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return (Math.random()*100000).toFixed(0);" - }, - { - "name": "smsApiState", - "type": "timeseries", - "label": "title", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.286155312985084, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.sms}\";" - }, - { - "name": "smsApiState", - "type": "timeseries", - "label": "unit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.39363366156190605, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.messages}\";" - } - ] - } - ], - "timewindow": { - "realtime": { - "timewindowMs": 60000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "#666666", - "padding": "0", - "settings": { - "cardHtml": "
\n \n \n
\n
\n
\n
${title}
\n
${apiState}
\n
\n
\n
${unit}
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
", - "cardCss": ".card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 13px 13px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n\n.card .mat-button {\n text-transform: uppercase;\n}\n\n.card .mat-button-wrapper {\n pointer-events: none;\n}\n\n.card .action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-button {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 10px;\n }\n .card .mat-button {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-button {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} \n\n\n" - }, - "title": "SMS messages", - "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": { - "cursor": "default" - }, - "titleStyle": { - "fontSize": "20px", - "fontWeight": 400 - }, - "useDashboardTimewindow": true, - "showLegend": false, - "actions": { - "elementClick": [ - { - "name": "sms_messages_details", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "sms_messages", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "73a35536-96a3-47dd-95bb-b1d9119d0ea2" - } - ] - }, - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "" - }, - "row": 0, - "col": 0, - "id": "2503a391-5692-0614-85e6-f179c5ee0dc9" - }, "68e16e98-0420-f72c-4848-41dedffd3904": { "isSystemType": true, "bundleAlias": "charts", @@ -1625,9 +1473,9 @@ "filterId": null, "dataKeys": [ { - "name": "emailCountHourly", + "name": "createdAlarmsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.email-messages}", + "label": "{i18n:api-usage.alarms-created}", "color": "#d35a00", "settings": { "excludeFromStacking": false, @@ -1638,7 +1486,7 @@ "fillLines": false, "showPoints": false, "showPointShape": "circle", - "pointShapeFormatter": "", + "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", "showPointsLineWidth": 5, "showPointsRadius": 3, "showSeparateAxis": false, @@ -1712,7 +1560,7 @@ "showLabels": true } }, - "title": "{i18n:api-usage.email-messages-hourly-activity}", + "title": "{i18n:api-usage.alarms-created-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": { @@ -1728,9 +1576,14 @@ "name": "{i18n:api-usage.view-details}", "icon": "insert_chart", "type": "openDashboardState", - "targetDashboardStateId": "email_messages", + "targetDashboardStateId": "alarms_created", "setEntityId": false, "stateEntityParamName": null, + "openInSeparateDialog": null, + "dialogTitle": null, + "dialogHideDashboardToolbar": true, + "dialogWidth": null, + "dialogHeight": null, "openRightLayout": false, "id": "371882f9-ea23-3abc-fca8-9449c5dfdd6b" } @@ -1771,6 +1624,41 @@ "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", "filterId": null, "dataKeys": [ + { + "name": "emailCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.email-messages}", + "color": "#4caf50", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + } + }, + "_hash": 0.1348755140779876, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, { "name": "smsCountHourly", "type": "timeseries", @@ -1859,7 +1747,7 @@ "showLabels": true } }, - "title": "{i18n:api-usage.sms-messages-hourly-activity}", + "title": "{i18n:api-usage.notifications-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": { @@ -1875,9 +1763,14 @@ "name": "{i18n:api-usage.view-details}", "icon": "insert_chart", "type": "openDashboardState", - "targetDashboardStateId": "sms_messages", + "targetDashboardStateId": "notifications", "setEntityId": false, "stateEntityParamName": null, + "openInSeparateDialog": null, + "dialogTitle": null, + "dialogHideDashboardToolbar": true, + "dialogWidth": null, + "dialogHeight": null, "openRightLayout": false, "id": "49aefac0-ec5e-d6f3-f39c-8744759f4b19" } @@ -3071,9 +2964,9 @@ "filterId": null, "dataKeys": [ { - "name": "emailCountHourly", + "name": "createdAlarmsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.email-messages}", + "label": "{i18n:api-usage.alarms-created}", "color": "#d35a00", "settings": { "excludeFromStacking": false, @@ -3084,7 +2977,7 @@ "fillLines": false, "showPoints": false, "showPointShape": "circle", - "pointShapeFormatter": "", + "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", "showPointsLineWidth": 5, "showPointsRadius": 3, "showSeparateAxis": false, @@ -3159,7 +3052,7 @@ "showLabels": true } }, - "title": "{i18n:api-usage.email-messages-daily-activity}", + "title": "{i18n:api-usage.alarms-created-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": { @@ -3206,9 +3099,9 @@ "filterId": null, "dataKeys": [ { - "name": "emailCount", + "name": "createdAlarmsCount", "type": "timeseries", - "label": "{i18n:api-usage.email-messages}", + "label": "{i18n:api-usage.alarms-created}", "color": "#d35a00", "settings": { "excludeFromStacking": false, @@ -3219,7 +3112,7 @@ "fillLines": false, "showPoints": false, "showPointShape": "circle", - "pointShapeFormatter": "", + "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", "showPointsLineWidth": 5, "showPointsRadius": 3, "showSeparateAxis": false, @@ -3294,7 +3187,7 @@ "showLabels": true } }, - "title": "{i18n:api-usage.sms-messages-monthly-activity}", + "title": "{i18n:api-usage.alarms-created-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": { @@ -4027,65 +3920,511 @@ "displayTimewindow": true }, "id": "a669cf86-e715-efa4-dd9a-b839abf499e9" - } - }, - "states": { - "default": { - "name": "{i18n:api-usage.api-usage}", - "root": true, - "layouts": { - "main": { - "widgets": { - "fd6df872-2ddf-0921-3929-2e7f55062fad": { - "sizeX": 4, - "sizeY": 2, - "row": 0, - "col": 8, - "mobileHeight": 3 - }, - "7e235874-461b-e7c2-2fdd-d8762a020773": { - "sizeX": 4, - "sizeY": 2, - "row": 0, - "col": 12, - "mobileHeight": 3 - }, - "08545554-a0e8-05c7-66df-6000cfeff8a4": { - "sizeX": 4, - "sizeY": 2, - "row": 0, - "col": 4, - "mobileHeight": 3 - }, - "a245c67e-53ec-d299-fa89-69fe2062ccb2": { - "sizeX": 4, - "sizeY": 2, - "row": 0, - "col": 0, - "mobileHeight": 3 - }, - "a151ae60-0326-6116-d818-9070dda8e9c7": { - "sizeX": 4, - "sizeY": 2, - "row": 0, - "col": 16, - "mobileHeight": 3 - }, - "2503a391-5692-0614-85e6-f179c5ee0dc9": { - "sizeX": 4, - "sizeY": 2, - "row": 0, - "col": 20, - "mobileHeight": 3 - }, - "68e16e98-0420-f72c-4848-41dedffd3904": { - "sizeX": 8, - "sizeY": 4, - "row": 2, - "col": 8 - }, - "2aa6b499-6e27-b315-6833-89c4d58485ce": { - "sizeX": 8, + }, + "292eaded-4775-36f7-c896-98d57bdda936": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "html_value_card", + "type": "latest", + "title": "New widget", + "sizeX": 7.5, + "sizeY": 3, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "emailApiState", + "type": "timeseries", + "label": "apiState", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8830669138660703, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "emailLimit", + "type": "timeseries", + "label": "limit", + "color": "#4caf50", + "settings": {}, + "_hash": 0.5463603803546802, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "emailCount", + "type": "timeseries", + "label": "count", + "color": "#f44336", + "settings": {}, + "_hash": 0.5564241862015964, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "transportApiState", + "type": "timeseries", + "label": "cardId", + "color": "#607d8b", + "settings": {}, + "_hash": 0.051659774305067296, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return (Math.random()*100000).toFixed(0);" + }, + { + "name": "smsApiState", + "type": "timeseries", + "label": "apiStatePoint", + "color": "#e91e63", + "settings": {}, + "_hash": 0.2969682764607864, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "smsLimit", + "type": "timeseries", + "label": "pointsLimit", + "color": "#9c27b0", + "settings": {}, + "_hash": 0.22082255831864894, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "smsCount", + "type": "timeseries", + "label": "pointsCount", + "color": "#8bc34a", + "settings": {}, + "_hash": 0.6340356364819146, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "transportApiState", + "type": "timeseries", + "label": "title", + "color": "#3f51b5", + "settings": {}, + "_hash": 0.6894070537030252, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return \"{i18n:api-usage.notifications}\";" + } + ] + } + ], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "#666666", + "padding": "0", + "settings": { + "cardCss": ".card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 13px 13px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bars-row {\n flex: 1;\n display: flex;\n flex-direction: row;\n}\n\n.card .bar-column {\n flex: 1;\n display: flex;\n flex-direction: column;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n.card .mat-button {\n text-transform: uppercase;\n}\n\n.card .mat-button-wrapper {\n pointer-events: none;\n}\n\n.card .action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 6px;\n }\n .card .mat-button {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-button {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-button {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} \n\n", + "cardHtml": "
\n \n \n
\n
\n
\n
\n ${title}\n
\n
\n
\n
\n
\n
\n
{i18n:api-usage.email}
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
{i18n:api-usage.sms}
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
" + }, + "title": "Notifications (Email/SMS)", + "dropShadow": true, + "enableFullscreen": false, + "widgetStyle": { + "cursor": "default" + }, + "titleStyle": { + "fontSize": "20px", + "fontWeight": 400 + }, + "useDashboardTimewindow": true, + "showLegend": false, + "actions": { + "elementClick": [ + { + "name": "transport_details", + "icon": "insert_chart", + "type": "openDashboardState", + "targetDashboardStateId": "notifications", + "setEntityId": false, + "stateEntityParamName": null, + "openInSeparateDialog": null, + "dialogTitle": null, + "dialogHideDashboardToolbar": true, + "dialogWidth": null, + "dialogHeight": null, + "openRightLayout": false, + "id": "46b7cefe-e1f2-67c1-4055-3a214520f869" + } + ] + }, + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "" + }, + "row": 0, + "col": 0, + "id": "292eaded-4775-36f7-c896-98d57bdda936" + }, + "b3571a36-2106-1122-7d58-0d38bbb0e9c8": { + "isSystemType": true, + "bundleAlias": "charts", + "typeAlias": "timeseries_bars_flot", + "type": "timeseries", + "title": "New widget", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "emailCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.email-messages}", + "color": "#d35a00", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + } + }, + "_hash": 0.0661644137210089, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "hideInterval": false, + "hideAggregation": false, + "hideAggInterval": false, + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000 + }, + "aggregation": { + "type": "SUM", + "limit": 1000 + } + }, + "showTitle": true, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "8px", + "settings": { + "shadowSize": 4, + "fontColor": "#545454", + "fontSize": 10, + "xaxis": { + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "showLabels": true, + "color": "#545454", + "min": 0, + "tickDecimals": 0, + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;" + }, + "grid": { + "color": "#545454", + "tickColor": "#DDDDDD", + "verticalLines": true, + "horizontalLines": true, + "outlineWidth": 1 + }, + "stack": false, + "tooltipIndividual": false, + "defaultBarWidth": 1800000, + "barAlignment": "left", + "timeForComparison": "months", + "xaxisSecond": { + "axisPosition": "top", + "showLabels": true + } + }, + "title": "{i18n:api-usage.email-messages-daily-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "widgetStyle": {}, + "useDashboardTimewindow": false, + "showLegend": true, + "actions": {}, + "displayTimewindow": true, + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true + } + }, + "row": 0, + "col": 0, + "id": "b3571a36-2106-1122-7d58-0d38bbb0e9c8" + }, + "aa875b7f-e7c8-7529-1ae7-f456211b59cc": { + "isSystemType": true, + "bundleAlias": "charts", + "typeAlias": "timeseries_bars_flot", + "type": "timeseries", + "title": "New widget", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "emailCount", + "type": "timeseries", + "label": "{i18n:api-usage.email-messages}", + "color": "#d35a00", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + } + }, + "_hash": 0.0661644137210089, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "hideInterval": false, + "hideAggregation": false, + "hideAggInterval": false, + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 31536000000, + "interval": 1000 + }, + "aggregation": { + "type": "NONE", + "limit": 1000 + } + }, + "showTitle": true, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "8px", + "settings": { + "shadowSize": 4, + "fontColor": "#545454", + "fontSize": 10, + "xaxis": { + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "showLabels": true, + "color": "#545454", + "min": 0, + "tickDecimals": 0, + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;" + }, + "grid": { + "color": "#545454", + "tickColor": "#DDDDDD", + "verticalLines": true, + "horizontalLines": true, + "outlineWidth": 1 + }, + "stack": false, + "tooltipIndividual": false, + "defaultBarWidth": 900000000, + "barAlignment": "left", + "timeForComparison": "months", + "xaxisSecond": { + "axisPosition": "top", + "showLabels": true + } + }, + "title": "{i18n:api-usage.email-messages-monthly-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "widgetStyle": {}, + "useDashboardTimewindow": false, + "showLegend": true, + "actions": {}, + "displayTimewindow": true, + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true + } + }, + "row": 0, + "col": 0, + "id": "aa875b7f-e7c8-7529-1ae7-f456211b59cc" + } + }, + "states": { + "default": { + "name": "{i18n:api-usage.api-usage}", + "root": true, + "layouts": { + "main": { + "widgets": { + "fd6df872-2ddf-0921-3929-2e7f55062fad": { + "sizeX": 4, + "sizeY": 2, + "row": 0, + "col": 8, + "mobileHeight": 3 + }, + "7e235874-461b-e7c2-2fdd-d8762a020773": { + "sizeX": 4, + "sizeY": 2, + "row": 0, + "col": 12, + "mobileHeight": 3 + }, + "08545554-a0e8-05c7-66df-6000cfeff8a4": { + "sizeX": 4, + "sizeY": 2, + "row": 0, + "col": 4, + "mobileHeight": 3 + }, + "a245c67e-53ec-d299-fa89-69fe2062ccb2": { + "sizeX": 4, + "sizeY": 2, + "row": 0, + "col": 0, + "mobileHeight": 3 + }, + "a151ae60-0326-6116-d818-9070dda8e9c7": { + "sizeX": 4, + "sizeY": 2, + "row": 0, + "col": 16, + "mobileHeight": 3 + }, + "292eaded-4775-36f7-c896-98d57bdda936": { + "sizeX": 4, + "sizeY": 2, + "row": 0, + "col": 20, + "mobileHeight": 3 + }, + "68e16e98-0420-f72c-4848-41dedffd3904": { + "sizeX": 8, + "sizeY": 4, + "row": 2, + "col": 8 + }, + "2aa6b499-6e27-b315-6833-89c4d58485ce": { + "sizeX": 8, "sizeY": 4, "row": 2, "col": 0 @@ -4261,22 +4600,30 @@ } } }, - "email_messages": { - "name": "{i18n:api-usage.email-messages}", + "rule_engine_statistics": { + "name": "{i18n:api-usage.rule-engine-statistics}", "root": false, "layouts": { "main": { "widgets": { - "4b798823-b97d-9d6a-59dc-fcafd897fc23": { - "sizeX": 24, - "sizeY": 6, + "2408ad30-163e-8221-08e1-a82b638be564": { + "sizeX": 12, + "sizeY": 7, + "mobileHeight": null, "row": 0, "col": 0 }, - "6a981580-7490-19dd-f937-b64cbf67a982": { + "e43dcfe1-b970-6a11-ce0e-5769f3eb5e88": { + "sizeX": 12, + "sizeY": 7, + "mobileHeight": null, + "row": 0, + "col": 12 + }, + "a669cf86-e715-efa4-dd9a-b839abf499e9": { "sizeX": 24, - "sizeY": 6, - "row": 6, + "sizeY": 5, + "row": 7, "col": 0 } }, @@ -4294,20 +4641,32 @@ } } }, - "sms_messages": { - "name": "{i18n:api-usage.sms-messages}", + "notifications": { + "name": "{i18n:api-usage.notifications-email-sms}", "root": false, "layouts": { "main": { "widgets": { "7302df65-1b0c-579e-bbdb-145126ae3392": { - "sizeX": 24, + "sizeX": 12, "sizeY": 6, "row": 0, - "col": 0 + "col": 12 }, "fdb385e7-14fe-fc9f-ebdc-b400f26fc66b": { - "sizeX": 24, + "sizeX": 12, + "sizeY": 6, + "row": 6, + "col": 12 + }, + "b3571a36-2106-1122-7d58-0d38bbb0e9c8": { + "sizeX": 12, + "sizeY": 6, + "row": 0, + "col": 0 + }, + "aa875b7f-e7c8-7529-1ae7-f456211b59cc": { + "sizeX": 12, "sizeY": 6, "row": 6, "col": 0 @@ -4327,30 +4686,22 @@ } } }, - "rule_engine_statistics": { - "name": "{i18n:api-usage.rule-engine-statistics}", + "alarms_created": { + "name": "{i18n:api-usage.alarms-created}", "root": false, "layouts": { "main": { "widgets": { - "2408ad30-163e-8221-08e1-a82b638be564": { - "sizeX": 12, - "sizeY": 7, - "mobileHeight": null, + "4b798823-b97d-9d6a-59dc-fcafd897fc23": { + "sizeX": 24, + "sizeY": 6, "row": 0, "col": 0 }, - "e43dcfe1-b970-6a11-ce0e-5769f3eb5e88": { - "sizeX": 12, - "sizeY": 7, - "mobileHeight": null, - "row": 0, - "col": 12 - }, - "a669cf86-e715-efa4-dd9a-b839abf499e9": { + "6a981580-7490-19dd-f937-b64cbf67a982": { "sizeX": 24, - "sizeY": 5, - "row": 7, + "sizeY": 6, + "row": 6, "col": 0 } }, diff --git a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html index 2bd7190831..5a59caf862 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html @@ -25,4 +25,15 @@ + + {{ 'device-profile.edrx-cycle' | translate }} + + + {{ 'device-profile.edrx-cycle-required' | translate }} + + + {{ 'device-profile.edrx-cycle-pattern' | translate }} + + diff --git a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts index f74029c2ff..1d98f5580b 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts @@ -14,16 +14,16 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { - DeviceTransportConfiguration, - DeviceTransportType, Lwm2mDeviceTransportConfiguration -} from '@shared/models/device.models'; -import {PowerMode, PowerModeTranslationMap} from "@home/components/profile/device/lwm2m/lwm2m-profile-config.models"; +import { DeviceTransportConfiguration, Lwm2mDeviceTransportConfiguration } from '@shared/models/device.models'; +import { PowerMode, PowerModeTranslationMap } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models'; +import { takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { isDefinedAndNotNull } from '@core/utils'; @Component({ selector: 'tb-lwm2m-device-transport-configuration', @@ -35,7 +35,7 @@ import {PowerMode, PowerModeTranslationMap} from "@home/components/profile/devic multi: true }] }) -export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { +export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy { lwm2mDeviceTransportConfigurationFormGroup: FormGroup; powerMods = Object.values(PowerMode); @@ -53,6 +53,7 @@ export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueA @Input() disabled: boolean; + private destroy$ = new Subject(); private propagateChange = (v: any) => { }; constructor(private store: Store, @@ -68,13 +69,35 @@ export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueA ngOnInit() { this.lwm2mDeviceTransportConfigurationFormGroup = this.fb.group({ - powerMode: [null] + powerMode: [null], + edrxCycle: [0] }); - this.lwm2mDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.lwm2mDeviceTransportConfigurationFormGroup.get('powerMode').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((powerMode: PowerMode) => { + if (powerMode === PowerMode.E_DRX) { + this.lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle').enable({emitEvent: false}); + this.lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle').patchValue(0, {emitEvent: false}); + this.lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle') + .setValidators([Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]); + } else { + this.lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle').disable({emitEvent: false}); + this.lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle').clearValidators(); + } + this.lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle').updateValueAndValidity({emitEvent: false}); + }); + this.lwm2mDeviceTransportConfigurationFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(() => { this.updateModel(); }); } + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { @@ -85,13 +108,18 @@ export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueA } writeValue(value: Lwm2mDeviceTransportConfiguration | null): void { - this.lwm2mDeviceTransportConfigurationFormGroup.patchValue(value, {emitEvent: false}); + if (isDefinedAndNotNull(value)) { + this.lwm2mDeviceTransportConfigurationFormGroup.get('powerMode').patchValue(value.powerMode, {emitEvent: false, onlySelf: true}); + this.lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle').patchValue(value.edrxCycle || 0, {emitEvent: false}); + } else { + this.lwm2mDeviceTransportConfigurationFormGroup.patchValue({powerMode: null, edrxCycle: 0}, {emitEvent: false}); + } } private updateModel() { let configuration: DeviceTransportConfiguration = null; if (this.lwm2mDeviceTransportConfigurationFormGroup.valid) { - configuration = this.lwm2mDeviceTransportConfigurationFormGroup.getRawValue(); + configuration = this.lwm2mDeviceTransportConfigurationFormGroup.value; // configuration.type = DeviceTransportType.LWM2M; } this.propagateChange(configuration); diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html index 75d6beaaed..c453493a2c 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html @@ -17,7 +17,7 @@ -->
-

device.device-credentials

+

{{ 'device.device-credentials' | translate }}

- + -
+
-
- - -
+
+
+ + +
+
+ +
+ + + {{ 'device.loading-device-credentials' | translate }} + +
+