Browse Source

Merge pull request #8938 from thingsboard/feature/check-connectivity-device

Added check connectivity in device
pull/9019/head
Andrew Shvayka 3 years ago
committed by GitHub
parent
commit
581c02bf6b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  2. 2
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  3. 107
      application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java
  4. 29
      application/src/main/resources/thingsboard.yml
  5. 340
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java
  6. 29
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java
  7. 34
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityConfiguration.java
  8. 26
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityInfo.java
  9. 268
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java
  10. 123
      dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java
  11. 6
      ui-ngx/src/app/app.component.ts
  12. 19
      ui-ngx/src/app/core/auth/auth.actions.ts
  13. 14
      ui-ngx/src/app/core/auth/auth.effects.ts
  14. 15
      ui-ngx/src/app/core/auth/auth.reducer.ts
  15. 6
      ui-ngx/src/app/core/auth/auth.selectors.ts
  16. 10
      ui-ngx/src/app/core/http/device.service.ts
  17. 24
      ui-ngx/src/app/core/utils.ts
  18. 9
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts
  19. 402
      ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html
  20. 177
      ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.scss
  21. 222
      ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts
  22. 6
      ui-ngx/src/app/modules/home/pages/device/device.component.html
  23. 4
      ui-ngx/src/app/modules/home/pages/device/device.module.ts
  24. 35
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  25. 42
      ui-ngx/src/app/shared/models/device.models.ts
  26. 9
      ui-ngx/src/app/shared/models/icon.models.ts
  27. 1
      ui-ngx/src/app/shared/models/user-settings.models.ts
  28. 1
      ui-ngx/src/assets/docker.svg
  29. 1
      ui-ngx/src/assets/linux.svg
  30. 28
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  31. 1
      ui-ngx/src/assets/macos.svg
  32. 1
      ui-ngx/src/assets/windows.svg
  33. 49
      ui-ngx/src/form.scss
  34. 21
      ui-ngx/src/typings/utils.d.ts

5
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -81,6 +81,7 @@ public class ThingsboardSecurityConfiguration {
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**";
public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code";
public static final String DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT = "/api/device-connectivity/mqtts/certificate/download";
@Autowired private ThingsboardErrorResponseHandler restAccessDeniedHandler;
@ -150,7 +151,8 @@ public class ThingsboardSecurityConfiguration {
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
List<String> pathsToSkip = new ArrayList<>(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS));
pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT,
PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT));
PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT,
DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT));
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
JwtTokenAuthenticationProcessingFilter filter
= new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher);
@ -228,6 +230,7 @@ public class ThingsboardSecurityConfiguration {
.antMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point
.antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
.antMatchers(MAIL_OAUTH2_PROCESSING_ENTRY_POINT).permitAll() // Mail oauth2 code processing url
.antMatchers(DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT).permitAll() // Mail oauth2 code processing url
.antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points
.and()
.authorizeRequests()

2
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -24,6 +24,7 @@ public class ControllerConstants {
protected static final String CUSTOMER_ID = "customerId";
protected static final String TENANT_ID = "tenantId";
protected static final String DEVICE_ID = "deviceId";
protected static final String PROTOCOL = "protocol";
protected static final String EDGE_ID = "edgeId";
protected static final String RPC_ID = "rpcId";
protected static final String ENTITY_ID = "entityId";
@ -34,6 +35,7 @@ public class ControllerConstants {
protected static final String DASHBOARD_ID_PARAM_DESCRIPTION = "A string value representing the dashboard id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String RPC_ID_PARAM_DESCRIPTION = "A string value representing the rpc id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String PROTOCOL_PARAM_DESCRIPTION = "A string value representing the device connectivity protocol. Possible values: 'mqtt', 'mqtts', 'http', 'https', 'coap', 'coaps'";
protected static final String ENTITY_VIEW_ID_PARAM_DESCRIPTION = "A string value representing the entity view id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String DEVICE_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the device profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";

107
application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java

@ -0,0 +1,107 @@
/**
* Copyright © 2016-2023 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.controller;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.dao.device.DeviceConnectivityService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URISyntaxException;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PROTOCOL;
import static org.thingsboard.server.controller.ControllerConstants.PROTOCOL_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.PEM_CERT_FILE_NAME;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class DeviceConnectivityController extends BaseController {
private final DeviceConnectivityService deviceConnectivityService;
private final SystemSecurityService systemSecurityService;
@ApiOperation(value = "Get commands to publish device telemetry (getDevicePublishTelemetryCommands)",
notes = "Fetch the list of commands to publish device telemetry based on device profile " +
"If the user has the authority of 'Tenant Administrator', the server checks that the device is owned by the same tenant. " +
"If the user has the authority of 'Customer User', the server checks that the device is assigned to the same customer. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK",
examples = @io.swagger.annotations.Example(
value = {
@io.swagger.annotations.ExampleProperty(
mediaType = "application/json",
value = "{\"http\":\"curl -v -X POST http://localhost:8080/api/v1/0ySs4FTOn5WU15XLmal8/telemetry --header Content-Type:application/json --data {temperature:25}\"," +
"\"mqtt\":\"mosquitto_pub -d -q 1 -h localhost -t v1/devices/me/telemetry -i myClient1 -u myUsername1 -P myPassword -m {temperature:25}\"," +
"\"coap\":\"coap-client -m POST coap://localhost:5683/api/v1/0ySs4FTOn5WU15XLmal8/telemetry -t json -e {temperature:25}\"}")}))})
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/device-connectivity/{deviceId}", method = RequestMethod.GET)
@ResponseBody
public JsonNode getDevicePublishTelemetryCommands(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable(DEVICE_ID) String strDeviceId, HttpServletRequest request) throws ThingsboardException, URISyntaxException {
checkParameter(DEVICE_ID, strDeviceId);
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
Device device = checkDeviceId(deviceId, Operation.READ_CREDENTIALS);
String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
return deviceConnectivityService.findDevicePublishTelemetryCommands(baseUrl, device);
}
@ApiOperation(value = "Download server certificate using file path defined in device.connectivity properties (downloadServerCertificate)", notes = "Download server certificate.")
@RequestMapping(value = "/device-connectivity/{protocol}/certificate/download", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadServerCertificate(@ApiParam(value = PROTOCOL_PARAM_DESCRIPTION)
@PathVariable(PROTOCOL) String protocol) throws ThingsboardException, IOException {
checkParameter(PROTOCOL, protocol);
var pemCert =
checkNotNull(deviceConnectivityService.getPemCertFile(protocol), protocol + " pem cert file is not found!");
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + PEM_CERT_FILE_NAME)
.header("x-filename", PEM_CERT_FILE_NAME)
.contentLength(pemCert.contentLength())
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(pemCert);
}
}

29
application/src/main/resources/thingsboard.yml

@ -987,6 +987,35 @@ transport:
enabled: "${TB_TRANSPORT_STATS_ENABLED:true}"
print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}"
# Device connectivity properties to publish telemetry
device:
connectivity:
http:
enabled: "${DEVICE_CONNECTIVITY_HTTP_ENABLED:true}"
host: "${DEVICE_CONNECTIVITY_HTTP_HOST:}"
port: "${DEVICE_CONNECTIVITY_HTTP_PORT:8080}"
https:
enabled: "${DEVICE_CONNECTIVITY_HTTPS_ENABLED:false}"
host: "${DEVICE_CONNECTIVITY_HTTPS_HOST:}"
port: "${DEVICE_CONNECTIVITY_HTTPS_PORT:443}"
mqtt:
enabled: "${DEVICE_CONNECTIVITY_MQTT_ENABLED:true}"
host: "${DEVICE_CONNECTIVITY_MQTT_HOST:}"
port: "${DEVICE_CONNECTIVITY_MQTT_PORT:1883}"
mqtts:
enabled: "${DEVICE_CONNECTIVITY_MQTTS_ENABLED:false}"
host: "${DEVICE_CONNECTIVITY_MQTTS_HOST:}"
port: "${DEVICE_CONNECTIVITY_MQTTS_PORT:8883}"
pem_cert_file: "${DEVICE_CONNECTIVITY_MQTT_SSL_PEM_CERT:mqttserver.pem}"
coap:
enabled: "${DEVICE_CONNECTIVITY_COAP_ENABLED:true}"
host: "${DEVICE_CONNECTIVITY_COAP_HOST:}"
port: "${DEVICE_CONNECTIVITY_COAP_PORT:5683}"
coaps:
enabled: "${DEVICE_CONNECTIVITY_COAPS_ENABLED:false}"
host: "${DEVICE_CONNECTIVITY_COAPS_HOST:}"
port: "${DEVICE_CONNECTIVITY_COAPS_PORT:5684}"
# Edges parameters
edges:
enabled: "${EDGES_ENABLED:true}"

340
application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java

@ -0,0 +1,340 @@
/**
* Copyright © 2016-2023 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.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.device.DeviceDao;
import org.thingsboard.server.dao.service.DaoSqlTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.DOCKER;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS;
@TestPropertySource(properties = {
"device.connectivity.https.enabled=true",
"device.connectivity.mqtts.enabled=true",
"device.connectivity.coaps.enabled=true",
})
@ContextConfiguration(classes = {DeviceConnectivityControllerTest.Config.class})
@DaoSqlTest
public class DeviceConnectivityControllerTest extends AbstractControllerTest {
static final TypeReference<PageData<Device>> PAGE_DATA_DEVICE_TYPE_REF = new TypeReference<>() {
};
private static final String DEVICE_TELEMETRY_TOPIC = "v1/devices/customTopic";
private static final String CHECK_DOCUMENTATION = "Check documentation";
ListeningExecutorService executor;
private Tenant savedTenant;
private User tenantAdmin;
private DeviceProfileId mqttDeviceProfileId;
private DeviceProfileId coapDeviceProfileId;
static class Config {
@Bean
@Primary
public DeviceDao deviceDao(DeviceDao deviceDao) {
return Mockito.mock(DeviceDao.class, AdditionalAnswers.delegatesTo(deviceDao));
}
}
@Before
public void beforeTest() throws Exception {
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass()));
loginSysAdmin();
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
savedTenant = doPost("/api/tenant", tenant, Tenant.class);
Assert.assertNotNull(savedTenant);
tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
DeviceProfile mqttProfile = new DeviceProfile();
mqttProfile.setName("Mqtt device profile");
mqttProfile.setType(DeviceProfileType.DEFAULT);
mqttProfile.setTransportType(DeviceTransportType.MQTT);
DeviceProfileData deviceProfileData = new DeviceProfileData();
deviceProfileData.setConfiguration(new DefaultDeviceProfileConfiguration());
MqttDeviceProfileTransportConfiguration transportConfiguration = new MqttDeviceProfileTransportConfiguration();
transportConfiguration.setDeviceTelemetryTopic(DEVICE_TELEMETRY_TOPIC);
deviceProfileData.setTransportConfiguration(transportConfiguration);
mqttProfile.setProfileData(deviceProfileData);
mqttProfile.setDefault(false);
mqttProfile.setDefaultRuleChainId(null);
mqttDeviceProfileId = doPost("/api/deviceProfile", mqttProfile, DeviceProfile.class).getId();
DeviceProfile coapProfile = new DeviceProfile();
coapProfile.setName("Coap device profile");
coapProfile.setType(DeviceProfileType.DEFAULT);
coapProfile.setTransportType(DeviceTransportType.COAP);
DeviceProfileData deviceProfileData2 = new DeviceProfileData();
deviceProfileData2.setConfiguration(new DefaultDeviceProfileConfiguration());
deviceProfileData2.setTransportConfiguration(new CoapDeviceProfileTransportConfiguration());
coapProfile.setProfileData(deviceProfileData);
coapProfile.setDefault(false);
coapProfile.setDefaultRuleChainId(null);
coapDeviceProfileId = doPost("/api/deviceProfile", coapProfile, DeviceProfile.class).getId();
}
@After
public void afterTest() throws Exception {
executor.shutdownNow();
loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testFetchPublishTelemetryCommandsForDefaultDevice() throws Exception {
Device device = new Device();
device.setName("My device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
assertThat(commands).hasSize(3);
JsonNode httpCommands = commands.get(HTTP);
assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost:8080/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost:443/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
JsonNode mqttCommands = commands.get(MQTT);
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t v1/devices/me/telemetry " +
"-u %s -m \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 " +
"-t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" +
" -p 1883 -t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 -t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"\"",
credentials.getCredentialsId()));
JsonNode linuxCoapCommands = commands.get(COAP);
assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -m POST coap://localhost:5683/api/v1/%s/telemetry " +
"-t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -m POST coaps://localhost:5684/api/v1/%s/telemetry" +
" -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
}
@Test
public void testFetchPublishTelemetryCommandsForMqttDeviceWithAccessToken() throws Exception {
Device device = new Device();
device.setName("My device");
device.setDeviceProfileId(mqttDeviceProfileId);
Device savedDevice = doPost("/api/device", device, Device.class);
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
assertThat(commands).hasSize(1);
JsonNode mqttCommands = commands.get(MQTT);
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " +
"-u %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 " +
"-t %s -u %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" +
" -p 1883 -t %s -u %s -m \"{temperature:25}\"",
DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 -t %s -u %s -m \"{temperature:25}\"\"",
DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
}
@Test
public void testFetchPublishTelemetryCommandsForDeviceWithMqttBasicCreds() throws Exception {
Device device = new Device();
device.setName("My device");
device.setDeviceProfileId(mqttDeviceProfileId);
Device savedDevice = doPost("/api/device", device, Device.class);
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
credentials.setCredentialsId(null);
credentials.setCredentialsType(DeviceCredentialsType.MQTT_BASIC);
BasicMqttCredentials basicMqttCredentials = new BasicMqttCredentials();
String clientId = "testClientId";
String userName = "testUsername";
String password = "testPassword";
basicMqttCredentials.setClientId(clientId);
basicMqttCredentials.setUserName(userName);
basicMqttCredentials.setPassword(password);
credentials.setCredentialsValue(JacksonUtil.toString(basicMqttCredentials));
doPost("/api/device/credentials", credentials)
.andExpect(status().isOk());
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
assertThat(commands).hasSize(1);
JsonNode mqttCommands = commands.get(MQTT);
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " +
"-i %s -u %s -P %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 " +
"-t %s -i %s -u %s -P %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" +
" -p 1883 -t %s -i %s -u %s -P %s -m \"{temperature:25}\"",
DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 -t %s -i %s -u %s -P %s -m \"{temperature:25}\"\"",
DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
}
@Test
public void testFetchPublishTelemetryCommandsForDeviceWithX509Creds() throws Exception {
Device device = new Device();
device.setName("My device");
device.setDeviceProfileId(mqttDeviceProfileId);
Device savedDevice = doPost("/api/device", device, Device.class);
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
credentials.setCredentialsId(null);
credentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE);
credentials.setCredentialsValue("testValue");
doPost("/api/device/credentials", credentials)
.andExpect(status().isOk());
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
assertThat(commands).hasSize(1);
assertThat(commands.get(MQTT).get(MQTTS).asText()).isEqualTo(CHECK_DOCUMENTATION);
assertThat(commands.get(MQTT).get(DOCKER)).isNull();
}
@Test
public void testFetchPublishTelemetryCommandsForCoapDevice() throws Exception {
Device device = new Device();
device.setName("My device");
device.setDeviceProfileId(coapDeviceProfileId);
Device savedDevice = doPost("/api/device", device, Device.class);
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
assertThat(commands).hasSize(1);
JsonNode linuxCommands = commands.get(COAP);
assertThat(linuxCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -m POST coap://localhost:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(linuxCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -m POST coaps://localhost:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"",
credentials.getCredentialsId()));
}
@Test
public void testFetchPublishTelemetryCommandsForCoapDeviceWithX509Creds() throws Exception {
Device device = new Device();
device.setName("My device");
device.setDeviceProfileId(coapDeviceProfileId);
Device savedDevice = doPost("/api/device", device, Device.class);
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
credentials.setCredentialsId(null);
credentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE);
credentials.setCredentialsValue("testValue");
doPost("/api/device/credentials", credentials)
.andExpect(status().isOk());
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
assertThat(commands).hasSize(1);
assertThat(commands.get(COAP).get(COAPS).asText()).isEqualTo(CHECK_DOCUMENTATION);
}
}

29
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.device;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.core.io.Resource;
import org.thingsboard.server.common.data.Device;
import java.net.URISyntaxException;
public interface DeviceConnectivityService {
JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException;
Resource getPemCertFile(String protocol);
}

34
dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityConfiguration.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.device;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
@Configuration
@ConfigurationProperties(prefix = "device")
@Data
public class DeviceConnectivityConfiguration {
private Map<String, DeviceConnectivityInfo> connectivity;
public boolean isEnabled(String protocol) {
var info = connectivity.get(protocol);
return info != null && info.isEnabled();
}
}

26
dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityInfo.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.device;
import lombok.Data;
@Data
public class DeviceConnectivityInfo {
private boolean enabled;
private String host;
private String port;
private String pemCertFile;
}

268
dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java

@ -0,0 +1,268 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.device;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.ResourceUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.util.DeviceConnectivityUtil;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.CHECK_DOCUMENTATION;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.DOCKER;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS;
@Service("DeviceConnectivityDaoService")
@Slf4j
public class DeviceConnectivityServiceImpl implements DeviceConnectivityService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_DEVICE_ID = "Incorrect deviceId ";
public static final String DEFAULT_DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry";
@Autowired
private DeviceCredentialsService deviceCredentialsService;
@Autowired
private DeviceProfileService deviceProfileService;
@Autowired
private DeviceConnectivityConfiguration deviceConnectivityConfiguration;
@Override
public JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException {
DeviceId deviceId = device.getId();
log.trace("Executing findDevicePublishTelemetryCommands [{}]", deviceId);
validateId(deviceId, INCORRECT_DEVICE_ID + deviceId);
DeviceCredentials creds = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), deviceId);
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId());
DeviceTransportType transportType = deviceProfile.getTransportType();
ObjectNode commands = JacksonUtil.newObjectNode();
switch (transportType) {
case DEFAULT:
Optional.ofNullable(getHttpTransportPublishCommands(baseUrl, creds))
.ifPresent(v -> commands.set(HTTP, v));
Optional.ofNullable(getMqttTransportPublishCommands(baseUrl, creds))
.ifPresent(v -> commands.set(MQTT, v));
Optional.ofNullable(getCoapTransportPublishCommands(baseUrl, creds))
.ifPresent(v -> commands.set(COAP, v));
break;
case MQTT:
MqttDeviceProfileTransportConfiguration transportConfiguration =
(MqttDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration();
//TODO: add sparkplug command with emulator (check SSL)
if (transportConfiguration.isSparkplug()) {
ObjectNode sparkplug = JacksonUtil.newObjectNode();
sparkplug.put("sparkplug", CHECK_DOCUMENTATION);
commands.set(MQTT, sparkplug);
} else {
String topicName = transportConfiguration.getDeviceTelemetryTopic();
Optional.ofNullable(getMqttTransportPublishCommands(baseUrl, topicName, creds))
.ifPresent(v -> commands.set(MQTT, v));
}
break;
case COAP:
Optional.ofNullable(getCoapTransportPublishCommands(baseUrl, creds))
.ifPresent(v -> commands.set(COAP, v));
break;
default:
commands.put(transportType.name(), CHECK_DOCUMENTATION);
}
return commands;
}
@Override
public Resource getPemCertFile(String protocol) {
String certFilePath = deviceConnectivityConfiguration.getConnectivity()
.get(protocol)
.getPemCertFile();
if (StringUtils.isNotBlank(certFilePath) && ResourceUtils.resourceExists(this, certFilePath)) {
return new ClassPathResource(certFilePath);
} else {
return null;
}
}
private JsonNode getHttpTransportPublishCommands(String defaultHostname, DeviceCredentials deviceCredentials) throws URISyntaxException {
ObjectNode httpCommands = JacksonUtil.newObjectNode();
Optional.ofNullable(getHttpPublishCommand(HTTP, defaultHostname, deviceCredentials))
.ifPresent(v -> httpCommands.put(HTTP, v));
Optional.ofNullable(getHttpPublishCommand(HTTPS, defaultHostname, deviceCredentials))
.ifPresent(v -> httpCommands.put(HTTPS, v));
return httpCommands.isEmpty() ? null : httpCommands;
}
private String getHttpPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(protocol);
if (properties == null || !properties.isEnabled() ||
deviceCredentials.getCredentialsType() != DeviceCredentialsType.ACCESS_TOKEN) {
return null;
}
String hostName = getHost(baseUrl, properties);
String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort();
return DeviceConnectivityUtil.getHttpPublishCommand(protocol, hostName, port, deviceCredentials);
}
private JsonNode getMqttTransportPublishCommands(String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException {
return getMqttTransportPublishCommands(baseUrl, DEFAULT_DEVICE_TELEMETRY_TOPIC, deviceCredentials);
}
private JsonNode getMqttTransportPublishCommands(String baseUrl, String topic, DeviceCredentials deviceCredentials) throws URISyntaxException {
ObjectNode mqttCommands = JacksonUtil.newObjectNode();
if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) {
mqttCommands.put(MQTTS, CHECK_DOCUMENTATION);
return mqttCommands;
}
ObjectNode dockerMqttCommands = JacksonUtil.newObjectNode();
if (deviceConnectivityConfiguration.isEnabled(MQTT)) {
Optional.ofNullable(getMqttPublishCommand(baseUrl, topic, deviceCredentials)).
ifPresent(v -> mqttCommands.put(MQTT, v));
Optional.ofNullable(getDockerMqttPublishCommand(MQTT, baseUrl, topic, deviceCredentials))
.ifPresent(v -> dockerMqttCommands.put(MQTT, v));
}
if (deviceConnectivityConfiguration.isEnabled(MQTTS)) {
List<String> mqttsPublishCommand = getMqttsPublishCommand(baseUrl, topic, deviceCredentials);
if (mqttsPublishCommand != null) {
ArrayNode arrayNode = mqttCommands.putArray(MQTTS);
mqttsPublishCommand.forEach(arrayNode::add);
}
Optional.ofNullable(getDockerMqttPublishCommand(MQTTS, baseUrl, topic, deviceCredentials))
.ifPresent(v -> dockerMqttCommands.put(MQTTS, v));
}
if (!dockerMqttCommands.isEmpty()) {
mqttCommands.set(DOCKER, dockerMqttCommands);
}
return mqttCommands.isEmpty() ? null : mqttCommands;
}
private String getMqttPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(MQTT);
String mqttHost = getHost(baseUrl, properties);
String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort();
return DeviceConnectivityUtil.getMqttPublishCommand(MQTT, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials);
}
private List<String> getMqttsPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(MQTTS);
String mqttHost = getHost(baseUrl, properties);
String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort();
String pubCommand = DeviceConnectivityUtil.getMqttPublishCommand(MQTTS, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials);
ArrayList<String> commands = new ArrayList<>();
if (pubCommand != null) {
commands.add(DeviceConnectivityUtil.getCurlPemCertCommand(baseUrl, MQTTS));
commands.add(pubCommand);
return commands;
}
return null;
}
private String getDockerMqttPublishCommand(String protocol, String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(protocol);
String mqttHost = getHost(baseUrl, properties);
String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort();
return DeviceConnectivityUtil.getDockerMqttPublishCommand(protocol, baseUrl, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials);
}
private JsonNode getCoapTransportPublishCommands(String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException {
ObjectNode coapCommands = JacksonUtil.newObjectNode();
if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) {
coapCommands.put(COAPS, CHECK_DOCUMENTATION);
return coapCommands;
}
ObjectNode dockerCoapCommands = JacksonUtil.newObjectNode();
if (deviceConnectivityConfiguration.isEnabled(COAP)) {
Optional.ofNullable(getCoapPublishCommand(COAP, baseUrl, deviceCredentials))
.ifPresent(v -> coapCommands.put(COAP, v));
Optional.ofNullable(getDockerCoapPublishCommand(COAP, baseUrl, deviceCredentials))
.ifPresent(v -> dockerCoapCommands.put(COAP, v));
}
if (deviceConnectivityConfiguration.isEnabled(COAPS)) {
Optional.ofNullable(getCoapPublishCommand(COAPS, baseUrl, deviceCredentials))
.ifPresent(v -> coapCommands.put(COAPS, v));
Optional.ofNullable(getDockerCoapPublishCommand(COAPS, baseUrl, deviceCredentials))
.ifPresent(v -> dockerCoapCommands.put(COAPS, v));
}
if (!dockerCoapCommands.isEmpty()) {
coapCommands.set(DOCKER, dockerCoapCommands);
}
return coapCommands.isEmpty() ? null : coapCommands;
}
private String getCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(protocol);
String hostName = getHost(baseUrl, properties);
String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort();
return DeviceConnectivityUtil.getCoapPublishCommand(protocol, hostName, port, deviceCredentials);
}
private String getDockerCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(protocol);
String host = getHost(baseUrl, properties);
String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort();
return DeviceConnectivityUtil.getDockerCoapPublishCommand(protocol, host, port, deviceCredentials);
}
private String getHost(String baseUrl, DeviceConnectivityInfo properties) throws URISyntaxException {
return properties.getHost().isEmpty() ? new URI(baseUrl).getHost() : properties.getHost();
}
}

123
dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java

@ -0,0 +1,123 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.util;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentials;
public class DeviceConnectivityUtil {
public static final String HTTP = "http";
public static final String HTTPS = "https";
public static final String MQTT = "mqtt";
public static final String LINUX = "linux";
public static final String WINDOWS = "windows";
public static final String DOCKER = "docker";
public static final String MQTTS = "mqtts";
public static final String COAP = "coap";
public static final String COAPS = "coaps";
public static final String PEM_CERT_FILE_NAME = "tb-server-chain.pem";
public static final String CHECK_DOCUMENTATION = "Check documentation";
public static final String JSON_EXAMPLE_PAYLOAD = "\"{temperature:25}\"";
public static final String DOCKER_RUN = "docker run --rm -it ";
public static final String MQTT_IMAGE = "thingsboard/mosquitto-clients ";
public static final String COAP_IMAGE = "thingsboard/coap-clients ";
public static String getHttpPublishCommand(String protocol, String host, String port, DeviceCredentials deviceCredentials) {
return String.format("curl -v -X POST %s://%s%s/api/v1/%s/telemetry --header Content-Type:application/json --data " + JSON_EXAMPLE_PAYLOAD,
protocol, host, port, deviceCredentials.getCredentialsId());
}
public static String getMqttPublishCommand(String protocol, String host, String port, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) {
StringBuilder command = new StringBuilder("mosquitto_pub -d -q 1");
if (MQTTS.equals(protocol)) {
command.append(" --cafile ").append(PEM_CERT_FILE_NAME);
}
command.append(" -h ").append(host).append(port == null ? "" : " -p " + port);
command.append(" -t ").append(deviceTelemetryTopic);
switch (deviceCredentials.getCredentialsType()) {
case ACCESS_TOKEN:
command.append(" -u ").append(deviceCredentials.getCredentialsId());
break;
case MQTT_BASIC:
BasicMqttCredentials credentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(),
BasicMqttCredentials.class);
if (credentials != null) {
if (credentials.getClientId() != null) {
command.append(" -i ").append(credentials.getClientId());
}
if (credentials.getUserName() != null) {
command.append(" -u ").append(credentials.getUserName());
}
if (credentials.getPassword() != null) {
command.append(" -P ").append(credentials.getPassword());
}
} else {
return null;
}
break;
default:
return null;
}
command.append(" -m " + JSON_EXAMPLE_PAYLOAD);
return command.toString();
}
public static String getDockerMqttPublishCommand(String protocol, String baseUrl, String host, String port, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) {
String mqttCommand = getMqttPublishCommand(protocol, host, port, deviceTelemetryTopic, deviceCredentials);
if (mqttCommand == null) {
return null;
}
StringBuilder mqttDockerCommand = new StringBuilder();
mqttDockerCommand.append(DOCKER_RUN).append(MQTT_IMAGE);
if (MQTTS.equals(protocol)) {
mqttDockerCommand.append("/bin/sh -c \"")
.append(getCurlPemCertCommand(baseUrl, protocol))
.append(" && ")
.append(mqttCommand)
.append("\"");
} else {
mqttDockerCommand.append(mqttCommand);
}
return mqttDockerCommand.toString();
}
public static String getCurlPemCertCommand(String baseUrl, String protocol) {
return String.format("curl -f -S -o %s %s/api/device-connectivity/%s/certificate/download", PEM_CERT_FILE_NAME, baseUrl, protocol);
}
public static String getCoapPublishCommand(String protocol, String host, String port, DeviceCredentials deviceCredentials) {
switch (deviceCredentials.getCredentialsType()) {
case ACCESS_TOKEN:
String client = COAPS.equals(protocol) ? "coap-client-openssl" : "coap-client";
return String.format("%s -m POST %s://%s%s/api/v1/%s/telemetry -t json -e %s",
client, protocol, host, port, deviceCredentials.getCredentialsId(), JSON_EXAMPLE_PAYLOAD);
default:
return null;
}
}
public static String getDockerCoapPublishCommand(String protocol, String host, String port, DeviceCredentials deviceCredentials) {
String coapCommand = getCoapPublishCommand(protocol, host, port, deviceCredentials);
return coapCommand != null ? String.format("%s%s%s", DOCKER_RUN, COAP_IMAGE, coapCommand) : null;
}
}

6
ui-ngx/src/app/app.component.ts

@ -30,7 +30,7 @@ import { combineLatest } from 'rxjs';
import { selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors';
import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators';
import { AuthService } from '@core/auth/auth.service';
import { svgIcons } from '@shared/models/icon.models';
import { svgIcons, svgIconsUrl } from '@shared/models/icon.models';
@Component({
selector: 'tb-root',
@ -65,6 +65,10 @@ export class AppComponent implements OnInit {
);
}
for (const svgIcon of Object.keys(svgIconsUrl)) {
this.matIconRegistry.addSvgIcon(svgIcon, this.domSanitizer.bypassSecurityTrustResourceUrl(svgIconsUrl[svgIcon]));
}
this.storageService.testLocalStorage();
this.setupTranslate();

19
ui-ngx/src/app/core/auth/auth.actions.ts

@ -17,6 +17,7 @@
import { Action } from '@ngrx/store';
import { User } from '@shared/models/user.model';
import { AuthPayload } from '@core/auth/auth.models';
import { UserSettings } from '@shared/models/user-settings.models';
export enum AuthActionTypes {
AUTHENTICATED = '[Auth] Authenticated',
@ -25,7 +26,9 @@ export enum AuthActionTypes {
UPDATE_USER_DETAILS = '[Auth] Update User Details',
UPDATE_LAST_PUBLIC_DASHBOARD_ID = '[Auth] Update Last Public Dashboard Id',
UPDATE_HAS_REPOSITORY = '[Auth] Change Has Repository',
UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section'
UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section',
PUT_USER_SETTINGS = '[Preferences] Put user settings',
DELETE_USER_SETTINGS = '[Preferences] Delete user settings',
}
export class ActionAuthAuthenticated implements Action {
@ -68,6 +71,18 @@ export class ActionPreferencesUpdateOpenedMenuSection implements Action {
constructor(readonly payload: { path: string; opened: boolean }) {}
}
export class ActionPreferencesPutUserSettings implements Action {
readonly type = AuthActionTypes.PUT_USER_SETTINGS;
constructor(readonly payload: Partial<UserSettings>) {}
}
export class ActionPreferencesDeleteUserSettings implements Action {
readonly type = AuthActionTypes.DELETE_USER_SETTINGS;
constructor(readonly payload: Array<NestedKeyOf<UserSettings>>) {}
}
export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated |
ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasRepository |
ActionPreferencesUpdateOpenedMenuSection;
ActionPreferencesUpdateOpenedMenuSection | ActionPreferencesPutUserSettings | ActionPreferencesDeleteUserSettings;

14
ui-ngx/src/app/core/auth/auth.effects.ts

@ -39,4 +39,18 @@ export class AuthEffects {
withLatestFrom(this.store.pipe(select(selectAuthState))),
mergeMap(([action, state]) => this.userSettingsService.putUserSettings({ openedMenuSections: state.userSettings.openedMenuSections }))
), {dispatch: false});
putUserSettings = createEffect(() => this.actions$.pipe(
ofType(
AuthActionTypes.PUT_USER_SETTINGS,
),
mergeMap((state) => this.userSettingsService.putUserSettings(state.payload))
), {dispatch: false});
deleteUserSettings = createEffect(() => this.actions$.pipe(
ofType(
AuthActionTypes.DELETE_USER_SETTINGS,
),
mergeMap((state) => this.userSettingsService.deleteUserSettings(state.payload))
), {dispatch: false});
}

15
ui-ngx/src/app/core/auth/auth.reducer.ts

@ -16,7 +16,8 @@
import { AuthPayload, AuthState } from './auth.models';
import { AuthActions, AuthActionTypes } from './auth.actions';
import { initialUserSettings } from '@shared/models/user-settings.models';
import { initialUserSettings, UserSettings } from '@shared/models/user-settings.models';
import { unset } from '@core/utils';
const emptyUserAuthState: AuthPayload = {
authUser: null,
@ -42,6 +43,7 @@ export const authReducer = (
state: AuthState = initialState,
action: AuthActions
): AuthState => {
let userSettings: UserSettings;
switch (action.type) {
case AuthActionTypes.AUTHENTICATED:
return { ...state, isAuthenticated: true, ...action.payload };
@ -71,7 +73,16 @@ export const authReducer = (
} else {
openedMenuSections.delete(action.payload.path);
}
const userSettings = {...state.userSettings, ...{ openedMenuSections: Array.from(openedMenuSections)}};
userSettings = {...state.userSettings, ...{ openedMenuSections: Array.from(openedMenuSections)}};
return { ...state, ...{ userSettings }};
case AuthActionTypes.PUT_USER_SETTINGS:
userSettings = {...state.userSettings, ...action.payload};
return { ...state, ...{ userSettings }};
case AuthActionTypes.DELETE_USER_SETTINGS:
userSettings = {...state.userSettings};
action.payload.forEach(path => unset(userSettings, path));
return { ...state, ...{ userSettings }};
default:

6
ui-ngx/src/app/core/auth/auth.selectors.ts

@ -21,6 +21,7 @@ import { AuthState } from './auth.models';
import { take } from 'rxjs/operators';
import { AuthUser } from '@shared/models/user.model';
import { UserSettings } from '@shared/models/user-settings.models';
import { getDescendantProp } from '@core/utils';
export const selectAuthState = createFeatureSelector< AuthState>(
'auth'
@ -76,6 +77,11 @@ export const selectUserSettings = createSelector(
(state: AuthState) => state.userSettings
);
export const selectUserSettingsProperty = (path: NestedKeyOf<UserSettings>) => createSelector(
selectAuthState,
(state: AuthState) => getDescendantProp(state.userSettings, path)
);
export const selectOpenedMenuSections = createSelector(
selectAuthState,
(state: AuthState) => state.userSettings.openedMenuSections

10
ui-ngx/src/app/core/http/device.service.ts

@ -25,8 +25,10 @@ import {
ClaimResult,
Device,
DeviceCredentials,
DeviceInfo, DeviceInfoQuery,
DeviceSearchQuery
DeviceInfo,
DeviceInfoQuery,
DeviceSearchQuery,
PublishTelemetryCommand
} from '@app/shared/models/device.models';
import { EntitySubtype } from '@app/shared/models/entity-type.models';
import { AuthService } from '@core/auth/auth.service';
@ -208,4 +210,8 @@ export class DeviceService {
return this.http.post<BulkImportResult>('/api/device/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config));
}
public getDevicePublishTelemetryCommands(deviceId: string, config?: RequestConfig): Observable<PublishTelemetryCommand> {
return this.http.get<PublishTelemetryCommand>(`/api/device-connectivity/${deviceId}`, defaultHttpOptionsFromConfig(config));
}
}

24
ui-ngx/src/app/core/utils.ts

@ -315,6 +315,8 @@ export const isEqual = (a: any, b: any): boolean => _.isEqual(a, b);
export const isEmpty = (a: any): boolean => _.isEmpty(a);
export const unset = (object: any, path: string | symbol): boolean => _.unset(object, path);
export const isEqualIgnoreUndefined = (a: any, b: any): boolean => {
if (a === b) {
return true;
@ -774,3 +776,25 @@ export function genNextLabel(name: string, datasources: Datasource[]): string {
}
return label;
}
export const getOS = (): string => {
const userAgent = window.navigator.userAgent.toLowerCase();
const macosPlatforms = /(macintosh|macintel|macppc|mac68k|macos|mac_powerpc)/i;
const windowsPlatforms = /(win32|win64|windows|wince)/i;
const iosPlatforms = /(iphone|ipad|ipod|darwin|ios)/i;
let os = null;
if (macosPlatforms.test(userAgent)) {
os = 'macos';
} else if (iosPlatforms.test(userAgent)) {
os = 'ios';
} else if (windowsPlatforms.test(userAgent)) {
os = 'windows';
} else if (/android/.test(userAgent)) {
os = 'android';
} else if (/linux/.test(userAgent)) {
os = 'linux';
}
return os;
};

9
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts

@ -23,7 +23,6 @@ import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { Device, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models';
import { MatStepper, StepperOrientation } from '@angular/material/stepper';
import { BaseData, HasId } from '@shared/models/base-data';
import { EntityType } from '@shared/models/entity-type.models';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@ -40,7 +39,7 @@ import { HttpErrorResponse } from '@angular/common/http';
templateUrl: './device-wizard-dialog.component.html',
styleUrls: ['./device-wizard-dialog.component.scss']
})
export class DeviceWizardDialogComponent extends DialogComponent<DeviceWizardDialogComponent, boolean> {
export class DeviceWizardDialogComponent extends DialogComponent<DeviceWizardDialogComponent, Device> {
@ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatStepper;
@ -64,7 +63,7 @@ export class DeviceWizardDialogComponent extends DialogComponent<DeviceWizardDia
constructor(protected store: Store<AppState>,
protected router: Router,
public dialogRef: MatDialogRef<DeviceWizardDialogComponent, boolean>,
public dialogRef: MatDialogRef<DeviceWizardDialogComponent, Device>,
private deviceService: DeviceService,
private breakpointObserver: BreakpointObserver,
private fb: FormBuilder) {
@ -121,7 +120,7 @@ export class DeviceWizardDialogComponent extends DialogComponent<DeviceWizardDia
add(): void {
if (this.allValid()) {
this.createDevice().subscribe(
() => this.dialogRef.close(true)
(device) => this.dialogRef.close(device)
);
}
}
@ -137,7 +136,7 @@ export class DeviceWizardDialogComponent extends DialogComponent<DeviceWizardDia
}
}
private createDevice(): Observable<BaseData<HasId>> {
private createDevice(): Observable<Device> {
const device: Device = {
name: this.deviceWizardFormGroup.get('name').value,
label: this.deviceWizardFormGroup.get('label').value,

402
ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html

@ -0,0 +1,402 @@
<!--
Copyright © 2016-2023 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.
-->
<mat-toolbar color="primary">
<h2 translate>{{ dialogTitle }}</h2>
<span fxFlex></span>
<!-- <div [tb-help]="'check-connectivity'"></div>-->
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<section *ngIf="loadedCommand; else loadingCommand" class="tb-form-panel no-padding no-border">
<tb-toggle-header #transportProtocol
value="{{ selectTransportType }}"
name="TransportProtocol"
useSelectOnMdLg="false"
appearance="fill"
[fxHide]="allowTransportType.size < 2">
<tb-toggle-option
*ngIf="allowTransportType.has(BasicTransportType.HTTP)"
[value]="BasicTransportType.HTTP">
{{ deviceTransportTypeTranslationMap.get(BasicTransportType.HTTP) | translate }}
</tb-toggle-option>
<tb-toggle-option
*ngIf="allowTransportType.has(DeviceTransportType.MQTT)"
[value]="DeviceTransportType.MQTT">
{{ deviceTransportTypeTranslationMap.get(DeviceTransportType.MQTT) | translate }}
</tb-toggle-option>
<tb-toggle-option
*ngIf="allowTransportType.has(DeviceTransportType.COAP)"
[value]="DeviceTransportType.COAP">
{{ deviceTransportTypeTranslationMap.get(DeviceTransportType.COAP) | translate }}
</tb-toggle-option>
<tb-toggle-option
*ngIf="allowTransportType.has(DeviceTransportType.SNMP)"
[value]="DeviceTransportType.SNMP">
{{ deviceTransportTypeTranslationMap.get(DeviceTransportType.SNMP) | translate }}
</tb-toggle-option>
<tb-toggle-option
*ngIf="allowTransportType.has(DeviceTransportType.LWM2M)"
[value]="DeviceTransportType.LWM2M">
{{ deviceTransportTypeTranslationMap.get(DeviceTransportType.LWM2M) | translate }}
</tb-toggle-option>
</tb-toggle-header>
<div class="tb-form-panel no-padding no-border" [fxHide]="!allowTransportType.size">
<ng-container [ngSwitch]="transportProtocol.value">
<ng-template [ngSwitchCase]="BasicTransportType.HTTP">
<div class="tb-no-data-text" translate>device.connectivity.use-following-instructions</div>
<mat-tab-group [(selectedIndex)]="httpTabIndex">
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="windows"></mat-icon>
Windows
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>device.connectivity.install-necessary-client-tools</div>
<div class="tb-install-instruction-text" translate>device.connectivity.install-curl-windows</div>
</div>
<ng-container
*ngTemplateOutlet="executeCommand; context: {cmd: {
noSecLabel: 'HTTP', noSec: commands.http.http,
secLabel: 'HTTPs', sec: commands.http.https,
doc: {text: '', href: ''}}}">
</ng-container>
</div>
</ng-template>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="macos"></mat-icon>
MacOS
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>device.connectivity.install-necessary-client-tools</div>
<tb-markdown usePlainMarkdown containerClass="tb-command-code"
[data]='createMarkDownCommand("brew install curl")'></tb-markdown>
</div>
<ng-container
*ngTemplateOutlet="executeCommand; context: {cmd: {
noSecLabel: 'HTTP', noSec: commands.http.http,
secLabel: 'HTTPs', sec: commands.http.https,
doc: {text: '', href: ''}}}">
</ng-container>
</div>
</ng-template>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="linux"></mat-icon>
Linux
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>device.connectivity.install-necessary-client-tools</div>
<tb-markdown usePlainMarkdown containerClass="tb-command-code"
[data]='createMarkDownCommand("sudo apt-get install curl")'></tb-markdown>
</div>
<ng-container
*ngTemplateOutlet="executeCommand; context: {cmd: {
noSecLabel: 'HTTP', noSec: commands.http.http,
secLabel: 'HTTPs', sec: commands.http.https,
doc: {text: '', href: ''}}}">
</ng-container>
</div>
</ng-template>
</mat-tab>
</mat-tab-group>
</ng-template>
<ng-template [ngSwitchCase]="DeviceTransportType.MQTT">
<div *ngIf="commands.mqtt.sparkplug; else mqttCommands" class="tb-form-panel stroked">
<ng-container
*ngTemplateOutlet="seeDocumentation; context:
{doc: {text: 'device.connectivity.sparkplug-command', href: 'https://thingsboard.io/docs/reference/mqtt-sparkplug-api/'}}">
</ng-container>
</div>
<ng-template #mqttCommands>
<div class="tb-no-data-text" translate>device.connectivity.use-following-instructions</div>
<mat-tab-group [(selectedIndex)]="mqttTabIndex">
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="windows"></mat-icon>
Windows
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>device.connectivity.install-necessary-client-tools</div>
<div class="tb-install-instruction-text">
<ng-container *ngTemplateOutlet="seeDocumentation; context:
{doc: {text: 'device.connectivity.install-mqtt-windows',
href: 'https://thingsboard.io/docs/getting-started-guides/helloworld/?connectdevice=mqtt-windows#step-2-connect-device'}}">
</ng-container>
</div>
</div>
<ng-container
*ngTemplateOutlet="executeCommand; context: {cmd: {
noSecLabel: 'MQTT', noSec: commands.mqtt.mqtt,
secLabel: 'MQTTs', sec: commands.mqtt.mqtts,
doc: {text: 'device.connectivity.mqtts-x509-command', href: 'https://thingsboard.io/docs/user-guide/mqtt-over-ssl/'}}}">
</ng-container>
</div>
</ng-template>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="macos"></mat-icon>
MacOS
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>device.connectivity.install-necessary-client-tools</div>
<tb-markdown usePlainMarkdown containerClass="tb-command-code"
[data]='createMarkDownCommand("brew install mosquitto-clients")'></tb-markdown>
</div>
<ng-container
*ngTemplateOutlet="executeCommand; context: {cmd: {
noSecLabel: 'MQTT', noSec: commands.mqtt.mqtt,
secLabel: 'MQTTs', sec: commands.mqtt.mqtts,
doc: {text: 'device.connectivity.mqtts-x509-command', href: 'https://thingsboard.io/docs/user-guide/mqtt-over-ssl/'}}}">
</ng-container>
</div>
</ng-template>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="linux"></mat-icon>
Linux
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>device.connectivity.install-necessary-client-tools</div>
<tb-markdown usePlainMarkdown containerClass="tb-command-code"
[data]='createMarkDownCommand("sudo apt-get install curl mosquitto-clients")'></tb-markdown>
</div>
<ng-container
*ngTemplateOutlet="executeCommand; context: {cmd: {
noSecLabel: 'MQTT', noSec: commands.mqtt.mqtt,
secLabel: 'MQTTs', sec: commands.mqtt.mqtts,
doc: {text: 'device.connectivity.mqtts-x509-command', href: 'https://thingsboard.io/docs/user-guide/mqtt-over-ssl/'}}}">
</ng-container>
</div>
</ng-template>
</mat-tab>
<mat-tab *ngIf="!!commands.mqtt.docker">
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="docker"></mat-icon>
Docker
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<ng-container
*ngTemplateOutlet="executeCommand; context: {cmd: {
noSecLabel: 'MQTT', noSec: commands.mqtt.docker.mqtt,
secLabel: 'MQTTs', sec: commands.mqtt.docker.mqtts,
doc: {text: 'device.connectivity.mqtts-x509-command', href: 'https://thingsboard.io/docs/user-guide/mqtt-over-ssl/'}}}">
</ng-container>
</div>
</ng-template>
</mat-tab>
</mat-tab-group>
</ng-template>
</ng-template>
<ng-template [ngSwitchCase]="DeviceTransportType.COAP">
<div class="tb-no-data-text" translate>device.connectivity.use-following-instructions</div>
<mat-tab-group [(selectedIndex)]="coapTabIndex">
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="macos"></mat-icon>
MacOS
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<div class="tb-form-panel stroked" *ngIf="commands.coap.coaps !== 'Check documentation'">
<div class="tb-form-panel-title" translate>device.connectivity.install-necessary-client-tools</div>
<div class="tb-install-instruction-text">
<ng-container *ngTemplateOutlet="seeDocumentation; context:
{doc: {text: 'device.connectivity.install-coap-client',
href: 'https://thingsboard.io/docs/user-guide/ssl/coap-access-token/'}}">
</ng-container>
</div>
</div>
<ng-container
*ngTemplateOutlet="executeCommand; context: {cmd: {
noSecLabel: 'CoAP', noSec: commands.coap.coap,
secLabel: 'CoAPs', sec: commands.coap.coaps,
doc: {text: 'device.connectivity.coaps-x509-command', href: 'https://thingsboard.io/docs/user-guide/ssl/coap-x509-certificates/'}}}">
</ng-container>
</div>
</ng-template>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="linux"></mat-icon>
Linux
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<div class="tb-form-panel stroked" *ngIf="commands.coap.coaps !== 'Check documentation'">
<div class="tb-form-panel-title" translate>device.connectivity.install-necessary-client-tools</div>
<div class="tb-install-instruction-text">
<ng-container *ngTemplateOutlet="seeDocumentation; context:
{doc: {text: 'device.connectivity.install-coap-client',
href: 'https://thingsboard.io/docs/user-guide/ssl/coap-access-token/'}}">
</ng-container>
</div>
</div>
<ng-container
*ngTemplateOutlet="executeCommand; context: {cmd: {
noSecLabel: 'CoAP', noSec: commands.coap.coap,
secLabel: 'CoAPs', sec: commands.coap.coaps,
doc: {text: 'device.connectivity.coaps-x509-command', href: 'https://thingsboard.io/docs/user-guide/ssl/coap-x509-certificates/'}}}">
</ng-container>
</div>
</ng-template>
</mat-tab>
<mat-tab *ngIf="!!commands.coap.docker">
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="docker"></mat-icon>
Docker
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<ng-container
*ngTemplateOutlet="executeCommand; context: {cmd: {
noSecLabel: 'CoAP', noSec: commands.coap.docker.coap,
secLabel: 'CoAPs', sec: commands.coap.docker.coaps,
doc: {text: 'device.connectivity.coaps-x509-command', href: 'https://thingsboard.io/docs/user-guide/ssl/coap-x509-certificates/'}}}">
</ng-container>
</div>
</ng-template>
</mat-tab>
</mat-tab-group>
</ng-template>
<ng-template [ngSwitchCase]="DeviceTransportType.SNMP">
<div class="tb-form-panel stroked">
<ng-container
*ngTemplateOutlet="seeDocumentation; context:
{doc: {text: 'device.connectivity.snmp-command', href: 'https://thingsboard.io/docs/reference/snmp-api/'}}">
</ng-container>
</div>
</ng-template>
<ng-template [ngSwitchCase]="DeviceTransportType.LWM2M">
<div class="tb-form-panel stroked">
<ng-container
*ngTemplateOutlet="seeDocumentation; context:
{doc: {text: 'device.connectivity.lwm2m-command', href: 'https://thingsboard.io/docs/reference/lwm2m-api/'}}">
</ng-container>
</div>
</ng-template>
</ng-container>
</div>
<div class="tb-form-panel stroked">
<div fxFlex fxLayout="row">
<div class="tb-form-panel-title" translate>device.state</div>
<div class="status" [ngClass]="{'inactive': !status, 'hide': status === undefined}">
{{ (status ? 'device.active' : 'device.inactive') | translate }}
</div>
</div>
<div class="tb-form-panel-hint tb-font-14" translate>attribute.latest-telemetry</div>
<div class="tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell" fxFlex translate>device.time</div>
<div class="tb-form-table-header-cell" fxFlex translate>attribute.key</div>
<div class="tb-form-table-header-cell" fxFlex translate>attribute.value</div>
</div>
<div *ngIf="latestTelemetry.length; else noData" class="tb-form-table-body">
<div class="tb-form-table-row" *ngFor="let telemetry of latestTelemetry">
<div class="tb-form-table-row-cell" fxFlex>{{ telemetry.lastUpdateTs | date: 'yyyy-MM-dd HH:mm:ss' }}</div>
<div class="tb-form-table-row-cell" fxFlex>{{ telemetry.key }}</div>
<div class="tb-form-table-row-cell" fxFlex>{{ telemetry.value }}</div>
</div>
</div>
</div>
</div>
</section>
</div>
<div mat-dialog-actions class="tb-dialog-actions">
<mat-slide-toggle fxShow="{{ showDontShowAgain }}" [(ngModel)]="notShowAgain">{{ 'action.dont-show-again' | translate}}</mat-slide-toggle>
<span fxFlex></span>
<button mat-button
[disabled]="(isLoading$ | async)"
(click)="close()">{{ closeButtonLabel | translate }}</button>
</div>
<ng-template #loadingCommand>
<div class="tb-loader">
<mat-spinner color="accent" diameter="65" strokeWidth="4"></mat-spinner>
<span class="mat-subtitle-1 label">
{{ 'device.connectivity.loading-check-connectivity-command' | translate }}
</span>
</div>
</ng-template>
<ng-template #noData>
<div class="tb-no-data-available">
<div class="tb-no-data-bg"></div>
<div class="tb-no-data-text" translate>attribute.no-latest-telemetry</div>
</div>
</ng-template>
<ng-template #executeCommand let-cmd="cmd">
<div class="tb-form-panel stroked">
<div fxLayout="row" fxLayoutAlign="space-between center" [fxHide]="!(cmd.noSec || cmd.sec !== 'Check documentation')">
<div class="tb-form-panel-title" translate>device.connectivity.execute-following-command</div>
<tb-toggle-header #protocolType value="{{ cmd.noSec ? 'noSec' : 'sec'}}" name="protocolType" useSelectOnMdLg="false"
[fxShow]="cmd.noSec && cmd.sec">
<tb-toggle-option value="noSec">{{ cmd.noSecLabel }}</tb-toggle-option>
<tb-toggle-option value="sec">{{ cmd.secLabel }}</tb-toggle-option>
</tb-toggle-header>
</div>
<ng-container [ngSwitch]="protocolType.value">
<ng-template [ngSwitchCase]="'noSec'">
<tb-markdown usePlainMarkdown containerClass="tb-command-code"
[data]=createMarkDownCommand(cmd.noSec)></tb-markdown>
</ng-template>
<ng-template [ngSwitchCase]="'sec'">
<div *ngIf="cmd.sec !== 'Check documentation'; else checkDocumentation">
<tb-markdown usePlainMarkdown containerClass="tb-command-code"
[data]=createMarkDownCommand(cmd.sec)></tb-markdown>
</div>
<ng-template #checkDocumentation>
<ng-container
*ngTemplateOutlet="seeDocumentation; context:
{doc: cmd.doc}">
</ng-container>
</ng-template>
</ng-template>
</ng-container>
</div>
</ng-template>
<ng-template #seeDocumentation let-doc="doc">
<div class="tb-form-row no-border no-padding space-between">
<div class="tb-font-14 tb-flex-1">{{ doc.text | translate }}</div>
<a mat-stroked-button color="primary" href="{{doc.href }}" target="_blank">
<mat-icon>description</mat-icon>
{{ 'common.documentation' | translate }}
</a>
</div>
</ng-template>

177
ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.scss

@ -0,0 +1,177 @@
/**
* Copyright © 2016-2023 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 "../../../../../scss/constants";
:host {
height: 100%;
max-height: 100vh;
display: grid;
grid-template-rows: min-content minmax(auto, 1fr) min-content;
.tb-loader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
height: 300px;
max-height: 100%;
.label {
margin-bottom: 0;
text-align: center;
}
}
.status {
margin-left: 12px;
border-radius: 12px;
height: 24px;
line-height: 24px;
padding: 0 8px;
width: fit-content;
color: #198038;
background-color: rgba(25, 128, 56, 0.08);
font-size: 14px;
&.inactive {
color: #d12730;
background-color: rgba(209, 39, 48, 0.08);
}
}
.tb-hint-instruction {
border-radius: 6px;
background-color: rgba(48, 86, 128, 0.04);
padding: 6px 16px;
.content {
vertical-align: middle;
}
}
.tb-font-14 {
font-size: 14px;
}
.tb-flex-1 {
flex: 1;
}
.tb-form-table-body {
max-height: 108px;
min-height: 108px;
overflow-y: auto;
scrollbar-gutter: stable;
.tb-form-table-row {
min-height: 38px;
}
}
.tb-no-data-available {
.tb-no-data-bg {
min-height: 68px;
}
}
.tb-install-instruction-text {
min-height: 42px;
}
@media #{$mat-sm} {
width: 470px;
}
@media #{$mat-gt-sm} {
width: 720px;
}
}
:host-context(.mat-mdc-dialog-container) {
.tb-dialog-actions {
display: flex;
gap: 8px;
padding: 8px 16px;
}
.mat-mdc-dialog-content {
max-height: 80vh;
padding: 16px;
}
}
:host ::ng-deep {
.tb-markdown-view {
.tb-command-code {
.code-wrapper {
padding: 0;
pre[class*=language-] {
margin: 0;
background: #F3F6FA;
border-color: #305680;
padding-right: 38px;
overflow: scroll;
padding-bottom: 4px;
&::-webkit-scrollbar {
width: 4px;
height: 4px;
}
}
}
button.clipboard-btn {
right: -2px;
p {
color: #305680;
}
p, div {
background-color: #F3F6FA;
}
div {
img {
display: none;
}
&:after {
content: "";
position: initial;
display: block;
width: 18px;
height: 18px;
background: #305680;
mask-image: url(/assets/copy-code-icon.svg);
mask-repeat: no-repeat;
}
}
}
}
}
.mdc-button__label > span {
.mat-icon {
vertical-align: text-bottom;
box-sizing: initial;
}
}
.tabs-icon {
margin-right: 8px;
}
.tb-form-panel.tb-tab-body {
padding: 16px 0 0;
}
}

222
ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts

@ -0,0 +1,222 @@
///
/// Copyright © 2016-2023 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, Inject, NgZone, OnDestroy, OnInit } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DeviceService } from '@core/http/device.service';
import {
AttributeData,
AttributeScope,
AttributesSubscriptionCmd,
LatestTelemetry,
TelemetrySubscriber
} from '@shared/models/telemetry/telemetry.models';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
import { selectPersistDeviceStateToTelemetry } from '@core/auth/auth.selectors';
import { take } from 'rxjs/operators';
import {
BasicTransportType,
DeviceTransportType,
deviceTransportTypeTranslationMap,
NetworkTransportType,
PublishTelemetryCommand
} from '@shared/models/device.models';
import { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { getOS } from '@core/utils';
export interface DeviceCheckConnectivityDialogData {
deviceId: EntityId;
afterAdd: boolean;
}
@Component({
selector: 'tb-device-check-connectivity-dialog',
templateUrl: './device-check-connectivity-dialog.component.html',
styleUrls: ['./device-check-connectivity-dialog.component.scss']
})
export class DeviceCheckConnectivityDialogComponent extends
DialogComponent<DeviceCheckConnectivityDialogComponent> implements OnInit, OnDestroy {
loadedCommand = false;
status: boolean;
latestTelemetry: Array<AttributeData> = [];
commands: PublishTelemetryCommand;
allowTransportType = new Set<NetworkTransportType>();
selectTransportType: NetworkTransportType;
BasicTransportType = BasicTransportType;
DeviceTransportType = DeviceTransportType;
deviceTransportTypeTranslationMap = deviceTransportTypeTranslationMap;
showDontShowAgain: boolean;
dialogTitle: string;
closeButtonLabel: string;
notShowAgain = false;
httpTabIndex = 0;
mqttTabIndex = 0;
coapTabIndex = 0;
private telemetrySubscriber: TelemetrySubscriber;
private currentTime = Date.now();
private transportTypes = [...Object.keys(BasicTransportType), ...Object.keys(DeviceTransportType)] as Array<NetworkTransportType>;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) private data: DeviceCheckConnectivityDialogData,
public dialogRef: MatDialogRef<DeviceCheckConnectivityDialogComponent>,
private deviceService: DeviceService,
private telemetryWsService: TelemetryWebsocketService,
private zone: NgZone) {
super(store, router, dialogRef);
if (this.data.afterAdd) {
this.dialogTitle = 'device.connectivity.device-created-check-connectivity';
this.closeButtonLabel = 'action.skip';
this.showDontShowAgain = true;
} else {
this.dialogTitle = 'device.connectivity.check-connectivity';
this.closeButtonLabel = 'action.close';
this.showDontShowAgain = false;
}
}
ngOnInit() {
this.loadCommands();
this.subscribeToLatestTelemetry();
}
ngOnDestroy() {
super.ngOnDestroy();
this.telemetrySubscriber?.complete();
this.telemetrySubscriber?.unsubscribe();
}
close(): void {
if (this.notShowAgain && this.showDontShowAgain) {
this.store.dispatch(new ActionPreferencesPutUserSettings({ notDisplayConnectivityAfterAddDevice: true }));
this.dialogRef.close(null);
} else {
this.dialogRef.close(null);
}
}
createMarkDownCommand(commands: string | string[]): string {
if (Array.isArray(commands)) {
const formatCommands: Array<string> = [];
commands.forEach(command => formatCommands.push(this.createMarkDownSingleCommand(command)));
return formatCommands.join(`\n<br />\n\n`);
} else {
return this.createMarkDownSingleCommand(commands);
}
}
private createMarkDownSingleCommand(command: string): string {
return '```bash\n' +
command +
'{:copy-code}\n' +
'```';
}
private loadCommands() {
this.deviceService.getDevicePublishTelemetryCommands(this.data.deviceId.id).subscribe(
commands => {
this.commands = commands;
const commandsProtocols = Object.keys(commands);
this.transportTypes.forEach(transport => {
const findCommand = commandsProtocols.find(item => item.toUpperCase().startsWith(transport));
if (findCommand) {
this.allowTransportType.add(transport);
}
});
this.selectTransportType = this.allowTransportType.values().next().value;
this.selectTabIndexForUserOS();
this.loadedCommand = true;
}
);
}
private subscribeToLatestTelemetry() {
this.store.pipe(select(selectPersistDeviceStateToTelemetry)).pipe(
take(1)
).subscribe(persistToTelemetry => {
this.telemetrySubscriber = TelemetrySubscriber.createEntityAttributesSubscription(
this.telemetryWsService, this.data.deviceId, LatestTelemetry.LATEST_TELEMETRY, this.zone);
if (!persistToTelemetry) {
const subscriptionCommand = new AttributesSubscriptionCmd();
subscriptionCommand.entityType = this.data.deviceId.entityType as EntityType;
subscriptionCommand.entityId = this.data.deviceId.id;
subscriptionCommand.scope = AttributeScope.SERVER_SCOPE;
subscriptionCommand.keys = 'active';
this.telemetrySubscriber.subscriptionCommands.push(subscriptionCommand);
}
this.telemetrySubscriber.subscribe();
this.telemetrySubscriber.attributeData$().subscribe(
(data) => {
const telemetry = data.reduce<Array<AttributeData>>((accumulator, item) => {
if (item.key === 'active') {
this.status = coerceBooleanProperty(item.value);
} else if (item.lastUpdateTs > this.currentTime) {
accumulator.push(item);
}
return accumulator;
}, []);
this.latestTelemetry = telemetry.sort((a, b) => b.lastUpdateTs - a.lastUpdateTs);
}
);
});
}
private selectTabIndexForUserOS() {
const currentOS = getOS();
switch (currentOS) {
case 'linux':
case 'android':
this.httpTabIndex = 2;
this.mqttTabIndex = 2;
this.coapTabIndex = 1;
break;
case 'macos':
case 'ios':
this.httpTabIndex = 1;
this.mqttTabIndex = 1;
break;
case 'windows':
this.httpTabIndex = 0;
this.mqttTabIndex = 0;
break;
default:
this.mqttTabIndex = this.commands.mqtt?.docker ? 3 : 0;
this.coapTabIndex = this.commands.coap?.docker ? 2 : 1;
}
}
}

6
ui-ngx/src/app/modules/home/pages/device/device.component.html

@ -46,6 +46,12 @@
[fxShow]="!isEdit">
{{ ((deviceScope === 'customer_user' || deviceScope === 'edge_customer_user') ? 'device.view-credentials' : 'device.manage-credentials') | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'checkConnectivity')"
[fxShow]="!isEdit">
{{ 'device.connectivity.check-connectivity' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'unassignFromEdge')"

4
ui-ngx/src/app/modules/home/pages/device/device.module.ts

@ -35,6 +35,7 @@ import { Lwm2mDeviceTransportConfigurationComponent } from './data/lwm2m-device-
import { SnmpDeviceTransportConfigurationComponent } from './data/snmp-device-transport-configuration.component';
import { DeviceCredentialsModule } from '@home/components/device/device-credentials.module';
import { DeviceProfileCommonModule } from '@home/components/profile/device/common/device-profile-common.module';
import { DeviceCheckConnectivityDialogComponent } from './device-check-connectivity-dialog.component';
@NgModule({
declarations: [
@ -50,7 +51,8 @@ import { DeviceProfileCommonModule } from '@home/components/profile/device/commo
DeviceComponent,
DeviceTabsComponent,
DeviceTableHeaderComponent,
DeviceCredentialsDialogComponent
DeviceCredentialsDialogComponent,
DeviceCheckConnectivityDialogComponent
],
imports: [
CommonModule,

35
ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts

@ -40,7 +40,7 @@ import {
import { DeviceComponent } from '@modules/home/pages/device/device.component';
import { forkJoin, Observable, of, Subject } from 'rxjs';
import { select, Store } from '@ngrx/store';
import { selectAuthUser } from '@core/auth/auth.selectors';
import { selectAuthUser, selectUserSettingsProperty } from '@core/auth/auth.selectors';
import { map, mergeMap, take, tap } from 'rxjs/operators';
import { AppState } from '@core/core.state';
import { DeviceService } from '@app/core/http/device.service';
@ -79,6 +79,11 @@ import { EdgeId } from '@shared/models/id/edge-id';
import { CustomerId } from '@shared/models/id/customer-id';
import { PageLink, PageQueryParam } from '@shared/models/page/page-link';
import { DeviceProfileId } from '@shared/models/id/device-profile-id';
import {
DeviceCheckConnectivityDialogComponent,
DeviceCheckConnectivityDialogData
} from '@home/pages/device/device-check-connectivity-dialog.component';
import { EntityId } from '@shared/models/id/entity-id';
interface DevicePageQueryParams extends PageQueryParam {
deviceProfileId?: string;
@ -456,13 +461,20 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
deviceWizard($event: Event) {
this.dialog.open<DeviceWizardDialogComponent, AddEntityDialogData<BaseData<HasId>>,
boolean>(DeviceWizardDialogComponent, {
Device>(DeviceWizardDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed().subscribe(
(res) => {
if (res) {
this.config.updateData();
this.store.pipe(select(selectUserSettingsProperty( 'notDisplayConnectivityAfterAddDevice'))).pipe(
take(1)
).subscribe((settings: boolean) => {
if(!settings) {
this.checkConnectivity(null, res.id, true);
}
});
}
}
);
@ -631,6 +643,9 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
case 'manageCredentials':
this.manageCredentials(action.event, action.entity);
return true;
case 'checkConnectivity':
this.checkConnectivity(action.event, action.entity.id);
return true;
}
return false;
}
@ -705,4 +720,20 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
);
}
checkConnectivity($event: Event, deviceId: EntityId, afterAdd = false) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<DeviceCheckConnectivityDialogComponent, DeviceCheckConnectivityDialogData>
(DeviceCheckConnectivityDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
deviceId,
afterAdd
}
})
.afterClosed()
.subscribe(() => {});
}
}

42
ui-ngx/src/app/shared/models/device.models.ts

@ -52,6 +52,12 @@ export enum DeviceTransportType {
SNMP = 'SNMP'
}
export enum BasicTransportType {
HTTP = 'HTTP'
}
export type TransportType = BasicTransportType | DeviceTransportType;
export type NetworkTransportType = BasicTransportType | Exclude<DeviceTransportType, DeviceTransportType.DEFAULT>;
export enum TransportPayloadType {
JSON = 'JSON',
PROTOBUF = 'PROTOBUF'
@ -99,13 +105,14 @@ export const deviceProfileTypeConfigurationInfoMap = new Map<DeviceProfileType,
]
);
export const deviceTransportTypeTranslationMap = new Map<DeviceTransportType, string>(
export const deviceTransportTypeTranslationMap = new Map<TransportType, string>(
[
[DeviceTransportType.DEFAULT, 'device-profile.transport-type-default'],
[DeviceTransportType.MQTT, 'device-profile.transport-type-mqtt'],
[DeviceTransportType.COAP, 'device-profile.transport-type-coap'],
[DeviceTransportType.LWM2M, 'device-profile.transport-type-lwm2m'],
[DeviceTransportType.SNMP, 'device-profile.transport-type-snmp']
[DeviceTransportType.SNMP, 'device-profile.transport-type-snmp'],
[BasicTransportType.HTTP, 'device-profile.transport-type-http']
]
);
@ -119,13 +126,14 @@ export const deviceProvisionTypeTranslationMap = new Map<DeviceProvisionType, st
]
);
export const deviceTransportTypeHintMap = new Map<DeviceTransportType, string>(
export const deviceTransportTypeHintMap = new Map<TransportType, string>(
[
[DeviceTransportType.DEFAULT, 'device-profile.transport-type-default-hint'],
[DeviceTransportType.MQTT, 'device-profile.transport-type-mqtt-hint'],
[DeviceTransportType.COAP, 'device-profile.transport-type-coap-hint'],
[DeviceTransportType.LWM2M, 'device-profile.transport-type-lwm2m-hint'],
[DeviceTransportType.SNMP, 'device-profile.transport-type-snmp-hint']
[DeviceTransportType.SNMP, 'device-profile.transport-type-snmp-hint'],
[BasicTransportType.HTTP, '']
]
);
@ -829,6 +837,32 @@ export interface ClaimResult {
response: ClaimResponse;
}
export interface PublishTelemetryCommand {
http?: {
http?: string;
https?: string;
};
mqtt: {
mqtt?: string;
mqtts?: string | Array<string>;
docker?: {
mqtt?: string;
mqtts?: string | Array<string>;
};
sparkplug?: string;
};
coap: {
coap?: string;
coaps?: string | Array<string>;
docker?: {
coap?: string;
coaps?: string | Array<string>;
};
};
lwm2m?: string;
snmp?: string;
}
export const dayOfWeekTranslations = new Array<string>(
'device-profile.schedule-day.monday',
'device-profile.schedule-day.tuesday',

9
ui-ngx/src/app/shared/models/icon.models.ts

@ -56,8 +56,15 @@ export const svgIcons: {[key: string]: string} = {
'</svg>'
};
export const svgIconsUrl: { [key: string]: string } = {
windows: '/assets/windows.svg',
macos: '/assets/macos.svg',
linux: '/assets/linux.svg',
docker: '/assets/docker.svg'
};
const svgIconNamespaces: string[] = ['mdi'];
const svgIconNames = Object.keys(svgIcons);
const svgIconNames = [...Object.keys(svgIcons), ...Object.keys(svgIconsUrl)];
export const splitIconName = (iconName: string): [string, string] => {
if (!iconName) {

1
ui-ngx/src/app/shared/models/user-settings.models.ts

@ -16,6 +16,7 @@
export interface UserSettings {
openedMenuSections?: string[];
notDisplayConnectivityAfterAddDevice?: boolean;
}
export const initialUserSettings: UserSettings = {

1
ui-ngx/src/assets/docker.svg

@ -0,0 +1 @@
<svg width="25" height="24" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M11.75 3a.25.25 0 0 0-.25.25v1.5c0 .138.112.25.25.25h1.5a.25.25 0 0 0 .25-.25v-1.5a.25.25 0 0 0-.25-.25h-1.5Zm-6 3a.25.25 0 0 0-.25.25v1.5c0 .138.112.25.25.25h1.5a.25.25 0 0 0 .25-.25v-1.5A.25.25 0 0 0 7.25 6h-1.5Zm3 0a.25.25 0 0 0-.25.25v1.5c0 .138.112.25.25.25h1.5a.25.25 0 0 0 .25-.25v-1.5a.25.25 0 0 0-.25-.25h-1.5Zm3 0a.25.25 0 0 0-.25.25v1.5c0 .138.112.25.25.25h1.5a.25.25 0 0 0 .25-.25v-1.5a.25.25 0 0 0-.25-.25h-1.5Zm7.49 1.15-.227.326c-.284.437-.49.918-.612 1.423-.23.967-.09 1.874.403 2.65-.596.33-1.611.444-1.804.451H1.5a1 1 0 0 0-1 1c0 1 .217 2.462.693 3.758.545 1.422 1.356 2.47 2.41 3.11 1.18.72 3.1 1.132 5.275 1.132.982.003 1.964-.086 2.93-.265a12.277 12.277 0 0 0 3.823-1.383 10.513 10.513 0 0 0 2.61-2.126c1.252-1.412 1.998-2.985 2.553-4.382l.007-.004c1.498.06 2.405-.519 2.894-1 .31-.292.55-.649.707-1.043l.098-.286-.236-.186c-.065-.051-.673-.509-1.954-.509-.339.002-.677.031-1.01.087-.248-1.692-1.653-2.518-1.716-2.555l-.345-.198ZM2.75 9a.25.25 0 0 0-.25.25v1.5c0 .138.112.25.25.25h1.5a.25.25 0 0 0 .25-.25v-1.5A.25.25 0 0 0 4.25 9h-1.5Zm3 0a.25.25 0 0 0-.25.25v1.5c0 .138.112.25.25.25h1.5a.25.25 0 0 0 .25-.25v-1.5A.25.25 0 0 0 7.25 9h-1.5Zm3 0a.25.25 0 0 0-.25.25v1.5c0 .138.112.25.25.25h1.5a.25.25 0 0 0 .25-.25v-1.5a.25.25 0 0 0-.25-.25h-1.5Zm3 0a.25.25 0 0 0-.25.25v1.5c0 .138.112.25.25.25h1.5a.25.25 0 0 0 .25-.25v-1.5a.25.25 0 0 0-.25-.25h-1.5Zm3 0a.25.25 0 0 0-.25.25v1.5c0 .138.112.25.25.25h1.5a.25.25 0 0 0 .25-.25v-1.5a.25.25 0 0 0-.25-.25h-1.5Z" fill="currentColor" /></g><defs><clipPath id="a"><path fill="currentColor" d="M.5 0h24v24H.5z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

1
ui-ngx/src/assets/linux.svg

@ -0,0 +1 @@
<svg width="25" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M20.91 18.862c-.652-.267-.931-.621-.904-1.15.028-.618-.323-1.07-.489-1.249.1-.384.394-1.712 0-2.866-.423-1.235-1.715-3.12-3.05-4.975-.545-.761-.57-1.589-.6-2.547-.03-.914-.062-1.95-.571-3.102C14.742 1.72 13.629 1 12.245 1c-.824 0-1.67.257-2.32.706-1.333.92-1.157 2.925-1.04 4.252.015.182.03.353.039.5.078 1.3.007 1.984-.085 2.193-.06.136-.354.523-.665.933-.322.424-.687.904-.986 1.352-.357.539-.645 1.362-.924 2.159-.204.583-.396 1.133-.584 1.462a1.991 1.991 0 0 0-.193 1.496c-.134.093-.329.277-.493.624-.198.423-.6.65-1.437.811-.384.079-.65.24-.788.48-.202.35-.092.79.008 1.09.149.44.056.72-.111 1.227-.04.116-.083.249-.127.395-.07.23-.045.44.074.623.316.483 1.238.653 2.187.765.567.068 1.187.294 1.787.514.587.214 1.195.436 1.747.504.084.01.168.016.248.016.834 0 1.21-.554 1.33-.781.3-.061 1.333-.257 2.398-.283 1.063-.03 2.091.18 2.383.245.091.175.333.575.718.782.212.115.506.182.808.182.322 0 .935-.076 1.42-.587.483-.512 1.692-1.167 2.575-1.644.197-.107.38-.207.542-.297.496-.275.767-.668.742-1.077-.02-.34-.245-.64-.588-.78Zm-10.975-.098c-.062-.435-.62-.866-1.269-1.365-.53-.409-1.13-.871-1.295-1.263-.341-.808-.072-2.23.398-2.962.232-.366.421-.922.605-1.46.198-.58.402-1.18.631-1.442.363-.41.698-1.208.757-1.837.34.324.867.735 1.353.735.075 0 .148-.01.217-.03.333-.095.823-.378 1.296-.652.409-.236.912-.526 1.102-.553.324.466 2.212 4.643 2.405 5.984a6.34 6.34 0 0 1-.09 2.283 1.678 1.678 0 0 0-.225-.016c-.525 0-.665.287-.7.458-.094.445-.104 1.869-.105 2.188-.19.242-1.151 1.38-2.531 1.584a10.96 10.96 0 0 1-1.56.123c-.405 0-.663-.032-.77-.048l-.694-.793c.274-.135.547-.42.475-.934Zm.88-13.085-.064.03a1.292 1.292 0 0 0-.014-.142c-.076-.436-.365-.752-.687-.752a.486.486 0 0 0-.075.005c-.191.032-.342.176-.424.38.072-.448.326-.78.627-.78.354 0 .653.477.653 1.042 0 .07-.005.142-.016.217Zm2.749.336c.032-.103.05-.215.05-.33 0-.512-.325-.913-.74-.913-.404 0-.734.41-.734.913 0 .034.002.068.005.102l-.063-.024a1.395 1.395 0 0 1-.07-.438c0-.612.391-1.11.872-1.11.48 0 .872.498.872 1.11 0 .254-.07.497-.192.69Zm-.354 1.19c-.007.032-.022.045-.185.13a4.979 4.979 0 0 0-.313.174l-.086.052c-.344.209-1.15.697-1.368.726-.149.02-.24-.037-.447-.178a6.4 6.4 0 0 0-.15-.1c-.372-.244-.612-.513-.639-.618.122-.094.423-.329.577-.468.313-.29.628-.486.783-.486.009 0 .016 0 .024.002.183.032.634.212.964.344.153.06.284.113.377.146.292.1.444.229.463.277ZM15.83 20.8c.165-.743.355-1.753.324-2.349-.007-.135-.019-.282-.03-.425-.022-.266-.054-.662-.021-.779a.145.145 0 0 1 .022-.008c.001.34.075 1.02.618 1.257.162.07.347.106.55.106.544 0 1.148-.267 1.395-.514.146-.146.268-.324.354-.465a.56.56 0 0 1 .024.22c-.032.502.212 1.168.676 1.413l.068.036c.165.087.605.317.612.427 0 0-.004.013-.028.035-.11.101-.498.299-.872.49-.665.34-1.418.724-1.757 1.08-.476.501-1.015.838-1.34.838a.354.354 0 0 1-.107-.015c-.353-.11-.644-.62-.487-1.347ZM3.789 18.905c-.036-.168-.064-.301-.034-.43.022-.096.494-.198.696-.242.283-.061.576-.125.767-.241.26-.157.4-.447.523-.702.09-.184.182-.375.292-.438a.061.061 0 0 1 .034-.008c.206 0 .64.434.888.822.064.097.18.293.316.52.406.678.96 1.607 1.25 1.919.262.28.685.818.58 1.28-.076.357-.481.648-.577.713a.59.59 0 0 1-.128.012c-.555 0-1.653-.462-2.243-.71l-.087-.037c-.33-.138-.868-.225-1.388-.309-.414-.067-.98-.159-1.074-.241-.076-.086.012-.364.09-.609.056-.176.114-.358.146-.548.045-.304-.008-.552-.05-.75Z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

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

@ -72,7 +72,9 @@
"skip": "Skip",
"send": "Send",
"reset": "Reset",
"show-more": "Show more"
"show-more": "Show more",
"dont-show-again": "Do not show again",
"see-documentation": "See documentation"
},
"aggregation": {
"aggregation": "Aggregation",
@ -694,6 +696,7 @@
"attribute": {
"attributes": "Attributes",
"latest-telemetry": "Latest telemetry",
"no-latest-telemetry": "No latest telemetry",
"attributes-scope": "Entity attributes scope",
"scope-latest-telemetry": "Latest telemetry",
"scope-client": "Client attributes",
@ -880,7 +883,8 @@
"loading": "Loading...",
"proceed": "Proceed",
"open-details-page": "Open details page",
"not-found": "Not found"
"not-found": "Not found",
"documentation": "Documentation"
},
"content-type": {
"json": "Json",
@ -1388,7 +1392,24 @@
"device-details": "Device details"
},
"unassign-devices-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 device} other {# devices} }?",
"unassign-devices-from-edge-text": "After the confirmation all selected devices will be unassigned and won't be accessible by the edge."
"unassign-devices-from-edge-text": "After the confirmation all selected devices will be unassigned and won't be accessible by the edge.",
"time": "Time",
"connectivity": {
"check-connectivity": "Check connectivity",
"device-created-check-connectivity": "Device created. Let's check connectivity!",
"loading-check-connectivity-command": "Loading check connectivity commands...",
"use-following-instructions": "Use the following instructions for sending telemetry on behalf of the device using shell",
"execute-following-command": "Execute the following command",
"install-curl-windows": "Starting Windows 10 b17063, cURL is available by default",
"install-mqtt-windows": "Use the instructions to download, install, setup and run mosquitto_pub",
"install-coap-client": "Use the instructions to download, install, setup and run coap-client",
"install-necessary-client-tools": "Install necessary client tools",
"mqtts-x509-command": "Use the following documentation to connect the device via MQTT with authorization X509",
"coaps-x509-command": "Use the following documentation to connect the device via CoAP over DTLS with authorization X509",
"snmp-command": "Use the following documentation to connect the device through the SNMP.",
"sparkplug-command": "Use the following documentation to connect the device through the MQTT Sparkplug.",
"lwm2m-command": "Use the following documentation to connect the device through the LWM2M."
}
},
"asset-profile": {
"asset-profile": "Asset profile",
@ -1470,6 +1491,7 @@
"transport-type-lwm2m-hint": "LWM2M transport type",
"transport-type-snmp": "SNMP",
"transport-type-snmp-hint": "Specify SNMP transport configuration",
"transport-type-http": "HTTP",
"description": "Description",
"default": "Default",
"profile-configuration": "Profile configuration",

1
ui-ngx/src/assets/macos.svg

@ -0,0 +1 @@
<svg width="25" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M21.873 16.68c-.517 1.15-.767 1.664-1.432 2.678-.932 1.418-2.247 3.188-3.87 3.199-1.446.013-1.82-.943-3.782-.926-1.961.009-2.37.945-3.818.93-1.625-.015-2.868-1.61-3.8-3.025-2.607-3.97-2.882-8.623-1.272-11.098 1.14-1.755 2.942-2.786 4.635-2.786 1.725 0 2.81.947 4.234.947 1.384 0 2.226-.95 4.22-.95 1.508 0 3.102.822 4.242 2.24-3.727 2.043-3.122 7.366.643 8.791Zm-6.4-12.615C16.2 3.134 16.752 1.82 16.55.48c-1.183.081-2.567.837-3.375 1.815-.733.893-1.34 2.217-1.104 3.497 1.292.042 2.628-.729 3.403-1.727Z" fill="currentColor" /></svg>

After

Width:  |  Height:  |  Size: 607 B

1
ui-ngx/src/assets/windows.svg

@ -0,0 +1 @@
<svg width="25" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M10.1 12.8H2.9V19l7.2.99V12.8Zm0-8.8L2.9 5v6.2h7.2V4Zm1.6-.2v7.4h10.4V2.4L11.7 3.8Zm0 9v7.4l10.4 1.4v-8.8H11.7Z" fill="currentColor" /></svg>

After

Width:  |  Height:  |  Size: 214 B

49
ui-ngx/src/form.scss

@ -330,24 +330,6 @@
gap: 12px;
padding-bottom: 12px;
.tb-form-table-header {
height: 48px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: row;
place-content: center flex-start;
align-items: center;
gap: 12px;
padding-left: 12px;
&-cell {
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.54);
}
}
.tb-form-table-body {
display: flex;
flex-direction: column;
@ -359,12 +341,31 @@
}
}
.tb-form-table-row {
height: 38px;
.tb-form-table-header, .tb-form-table-row {
display: flex;
flex-direction: row;
gap: 12px;
padding-left: 12px;
place-content: center flex-start;
align-items: center;
&-cell {
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.2px;
}
}
.tb-form-table-header {
height: 48px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
&-cell {
color: rgba(0, 0, 0, 0.54);
}
}
.tb-form-table-row {
height: 38px;
&.tb-draggable {
gap: 0;
@ -372,6 +373,10 @@
background: #fff;
}
&-cell {
color: rgba(0, 0, 0, 0.87);
}
&-cell-buttons {
display: flex;
flex-direction: row;
@ -411,10 +416,6 @@
right: 0;
bottom: 0;
background: #305680;
-webkit-mask-image: url(/assets/home/no_data_folder_bg.svg);
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
-webkit-mask-position: center;
mask-image: url(/assets/home/no_data_folder_bg.svg);
mask-repeat: no-repeat;
mask-size: contain;

21
ui-ngx/src/typings/utils.d.ts

@ -0,0 +1,21 @@
///
/// Copyright © 2016-2023 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.
///
type NestedKeyOf<ObjectType extends object> =
{[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]> extends infer U extends string ? U : never}`
: `${Key}`
}[keyof ObjectType & (string | number)];
Loading…
Cancel
Save