Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into feature/device-profile-telemetry-proto-schema

pull/3740/head
ShvaykaD 6 years ago
parent
commit
f0f30dc9c7
  1. 1
      application/src/main/data/json/system/oauth2_config_templates/github_config.json
  2. 3
      application/src/main/data/json/tenant/device_profile/rule_chain_template.json
  3. 6
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  4. 11
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  5. 2
      application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
  6. 18
      application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
  7. 4
      application/src/main/java/org/thingsboard/server/controller/TenantController.java
  8. 5
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  9. 2
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  10. 59
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java
  11. 12
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbTenantProfileCache.java
  12. 6
      application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java
  13. 4
      application/src/main/java/org/thingsboard/server/service/profile/TbTenantProfileCache.java
  14. 59
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  15. 13
      application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java
  16. 2
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  17. 22
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java
  18. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java
  19. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java
  20. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java
  21. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java
  22. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  23. 63
      application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
  24. 6
      application/src/main/resources/thingsboard.yml
  25. 6
      common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsDomainParams.java
  26. 6
      common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsParams.java
  27. 4
      common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java
  28. 26
      common/queue/src/main/java/org/thingsboard/server/queue/util/TbTransportComponent.java
  29. 47
      common/queue/src/main/proto/queue.proto
  30. 8
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  31. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportDeviceProfileCache.java
  32. 12
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
  33. 38
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportTenantProfileCache.java
  34. 47
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitFactory.java
  35. 133
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java
  36. 35
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DummyTransportRateLimit.java
  37. 38
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/SimpleTransportRateLimit.java
  38. 26
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimit.java
  39. 24
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitFactory.java
  40. 34
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java
  41. 47
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitType.java
  42. 30
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/profile/TenantProfileUpdateResult.java
  43. 9
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportDeviceProfileCache.java
  44. 192
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  45. 155
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportTenantProfileCache.java
  46. 24
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportTenantRoutingInfoService.java
  47. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/DataDecodingEncodingService.java
  48. 12
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java
  49. 93
      dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java
  50. 51
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
  51. 392
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  52. 7
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceProfileCache.java
  53. 5
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  54. 22
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java
  55. 30
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java
  56. 38
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java
  57. 64
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java
  58. 4
      transport/coap/src/main/resources/tb-coap-transport.yml
  59. 4
      transport/http/src/main/resources/tb-http-transport.yml
  60. 4
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml
  61. 4
      ui-ngx/src/app/core/http/oauth2.service.ts
  62. 5
      ui-ngx/src/app/core/services/menu.service.ts
  63. 17
      ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html
  64. 10
      ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html
  65. 10
      ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html
  66. 10
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html
  67. 15
      ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html
  68. 1
      ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html
  69. 10
      ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.html
  70. 10
      ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html
  71. 1
      ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html
  72. 12
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html
  73. 10
      ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html
  74. 2
      ui-ngx/src/app/modules/home/components/filter/filter-text.component.html
  75. 5
      ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss
  76. 3
      ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts
  77. 12
      ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html
  78. 10
      ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html
  79. 12
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html
  80. 10
      ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html
  81. 10
      ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html
  82. 45
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html
  83. 4
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss
  84. 12
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html
  85. 25
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html
  86. 7
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss
  87. 45
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html
  88. 30
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss
  89. 3
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts
  90. 12
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html
  91. 25
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.html
  92. 10
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.scss
  93. 2
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts
  94. 8
      ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html
  95. 21
      ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts
  96. 2
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html
  97. 4
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts
  98. 9
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts
  99. 10
      ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html
  100. 4
      ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts

1
application/src/main/data/json/system/oauth2_config_templates/github_config.json

@ -10,6 +10,7 @@
"mapperConfig": {
"type": "GITHUB",
"basic": {
"firstNameAttributeKey": "name",
"tenantNameStrategy": "DOMAIN"
}
},

3
application/src/main/data/json/tenant/device_profile/rule_chain_template.json

@ -94,7 +94,8 @@
"name": "Device Profile Node",
"debugMode": false,
"configuration": {
"persistAlarmRulesState": false
"persistAlarmRulesState": false,
"fetchAlarmRulesStateOnStart": false
}
}
],

6
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -544,7 +544,11 @@ public class ActorSystemContext {
public void scheduleMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs) {
log.debug("Scheduling msg {} with delay {} ms", msg, delayInMs);
getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS);
if (delayInMs > 0) {
getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS);
} else {
ctx.tell(msg);
}
}
}

11
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -35,6 +35,7 @@ import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.EntityId;
@ -477,6 +478,16 @@ class DefaultTbContext implements TbContext {
mainCtx.getRuleNodeStateService().removeByRuleNodeId(getTenantId(), getSelfId());
}
@Override
public void addProfileListener(Consumer<DeviceProfile> listener) {
mainCtx.getDeviceProfileCache().addListener(getTenantId(), getSelfId(), listener);
}
@Override
public void removeProfileListener() {
mainCtx.getDeviceProfileCache().removeListener(getTenantId(), getSelfId());
}
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ruleNodeId", ruleNodeId.toString());

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

@ -106,7 +106,6 @@ public class DeviceProfileController extends BaseController {
DeviceProfile savedDeviceProfile = checkNotNull(deviceProfileService.saveDeviceProfile(deviceProfile));
deviceProfileCache.put(savedDeviceProfile);
tbClusterService.onDeviceProfileChange(savedDeviceProfile, null);
tbClusterService.onEntityStateChange(deviceProfile.getTenantId(), savedDeviceProfile.getId(),
created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
@ -132,7 +131,6 @@ public class DeviceProfileController extends BaseController {
DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.DELETE);
deviceProfileService.deleteDeviceProfile(getTenantId(), deviceProfileId);
deviceProfileCache.evict(deviceProfileId);
tbClusterService.onDeviceProfileDelete(deviceProfile, null);
tbClusterService.onEntityStateChange(deviceProfile.getTenantId(), deviceProfile.getId(), ComponentLifecycleEvent.DELETED);

18
application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@ -23,6 +24,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams;
import org.thingsboard.server.common.data.oauth2.SchemeType;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
@ -36,6 +38,10 @@ import java.util.List;
@RequestMapping("/api")
@Slf4j
public class OAuth2Controller extends BaseController {
@Autowired
private OAuth2Configuration oAuth2Configuration;
@RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST)
@ResponseBody
public List<OAuth2ClientInfo> getOAuth2Clients(HttpServletRequest request) throws ThingsboardException {
@ -70,4 +76,16 @@ public class OAuth2Controller extends BaseController {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/oauth2/loginProcessingUrl", method = RequestMethod.GET)
@ResponseBody
public String getLoginProcessingUrl() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ);
return "\"" + oAuth2Configuration.getLoginProcessingUrl() + "\"";
} catch (Exception e) {
throw handleException(e);
}
}
}

4
application/src/main/java/org/thingsboard/server/controller/TenantController.java

@ -92,6 +92,7 @@ public class TenantController extends BaseController {
installScripts.createDefaultRuleChains(tenant.getId());
}
tenantProfileCache.evict(tenant.getId());
tbClusterService.onTenantChange(tenant, null);
return tenant;
} catch (Exception e) {
throw handleException(e);
@ -105,9 +106,10 @@ public class TenantController extends BaseController {
checkParameter("tenantId", strTenantId);
try {
TenantId tenantId = new TenantId(toUUID(strTenantId));
checkTenantId(tenantId, Operation.DELETE);
Tenant tenant = checkTenantId(tenantId, Operation.DELETE);
tenantService.deleteTenant(tenantId);
tenantProfileCache.evict(tenantId);
tbClusterService.onTenantDelete(tenant, null);
tbClusterService.onEntityStateChange(tenantId, tenantId, ComponentLifecycleEvent.DELETED);
} catch (Exception e) {
throw handleException(e);

5
application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java

@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
@ -96,6 +97,7 @@ public class TenantProfileController extends BaseController {
tenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(getTenantId(), tenantProfile));
tenantProfileCache.put(tenantProfile);
tbClusterService.onTenantProfileChange(tenantProfile, null);
tbClusterService.onEntityStateChange(TenantId.SYS_TENANT_ID, tenantProfile.getId(),
newTenantProfile ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
return tenantProfile;
@ -111,8 +113,9 @@ public class TenantProfileController extends BaseController {
checkParameter("tenantProfileId", strTenantProfileId);
try {
TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId));
checkTenantProfileId(tenantProfileId, Operation.DELETE);
TenantProfile profile = checkTenantProfileId(tenantProfileId, Operation.DELETE);
tenantProfileService.deleteTenantProfile(getTenantId(), tenantProfileId);
tbClusterService.onTenantProfileDelete(profile, null);
} catch (Exception e) {
throw handleException(e);
}

2
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -170,7 +170,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
ruleChainService.saveRuleChainMetaData(tenant.getId(), md);
}
} catch (Exception e) {
log.error("Unable to update Tenant", e);
log.error("[{}] Unable to update Tenant: {}", tenant.getId(), tenant.getName(), e);
}
}
};

59
application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java

@ -21,14 +21,17 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
@Service
@Slf4j
@ -40,6 +43,7 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache {
private final ConcurrentMap<DeviceProfileId, DeviceProfile> deviceProfilesMap = new ConcurrentHashMap<>();
private final ConcurrentMap<DeviceId, DeviceProfileId> devicesMap = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, ConcurrentMap<EntityId, Consumer<DeviceProfile>>> listeners = new ConcurrentHashMap<>();
public DefaultTbDeviceProfileCache(DeviceProfileService deviceProfileService, DeviceService deviceService) {
this.deviceProfileService = deviceProfileService;
@ -50,19 +54,21 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache {
public DeviceProfile get(TenantId tenantId, DeviceProfileId deviceProfileId) {
DeviceProfile profile = deviceProfilesMap.get(deviceProfileId);
if (profile == null) {
profile = deviceProfilesMap.get(deviceProfileId);
if (profile == null) {
deviceProfileFetchLock.lock();
try {
deviceProfileFetchLock.lock();
try {
profile = deviceProfilesMap.get(deviceProfileId);
if (profile == null) {
profile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId);
if (profile != null) {
deviceProfilesMap.put(deviceProfileId, profile);
log.debug("[{}] Fetch device profile into cache: {}", profile.getId(), profile);
}
} finally {
deviceProfileFetchLock.unlock();
}
} finally {
deviceProfileFetchLock.unlock();
}
}
log.trace("[{}] Found device profile in cache: {}", deviceProfileId, profile);
return profile;
}
@ -85,12 +91,19 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache {
public void put(DeviceProfile profile) {
if (profile.getId() != null) {
deviceProfilesMap.put(profile.getId(), profile);
log.debug("[{}] pushed device profile to cache: {}", profile.getId(), profile);
notifyListeners(profile);
}
}
@Override
public void evict(DeviceProfileId profileId) {
deviceProfilesMap.remove(profileId);
public void evict(TenantId tenantId, DeviceProfileId profileId) {
DeviceProfile oldProfile = deviceProfilesMap.remove(profileId);
log.debug("[{}] evict device profile from cache: {}", profileId, oldProfile);
DeviceProfile newProfile = get(tenantId, profileId);
if (newProfile != null) {
notifyListeners(newProfile);
}
}
@Override
@ -98,4 +111,34 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache {
devicesMap.remove(deviceId);
}
@Override
public void addListener(TenantId tenantId, EntityId listenerId, Consumer<DeviceProfile> listener) {
listeners.computeIfAbsent(tenantId, id -> new ConcurrentHashMap<>()).put(listenerId, listener);
}
@Override
public DeviceProfile find(DeviceProfileId deviceProfileId) {
return deviceProfileService.findDeviceProfileById(TenantId.SYS_TENANT_ID, deviceProfileId);
}
@Override
public DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName) {
return deviceProfileService.findOrCreateDeviceProfile(tenantId, profileName);
}
@Override
public void removeListener(TenantId tenantId, EntityId listenerId) {
ConcurrentMap<EntityId, Consumer<DeviceProfile>> tenantListeners = listeners.get(tenantId);
if (tenantListeners != null) {
tenantListeners.remove(listenerId);
}
}
private void notifyListeners(DeviceProfile profile) {
ConcurrentMap<EntityId, Consumer<DeviceProfile>> tenantListeners = listeners.get(profile.getTenantId());
if (tenantListeners != null) {
tenantListeners.forEach((id, listener) -> listener.accept(profile));
}
}
}

12
application/src/main/java/org/thingsboard/server/service/profile/DefaultTbTenantProfileCache.java

@ -55,17 +55,17 @@ public class DefaultTbTenantProfileCache implements TbTenantProfileCache {
public TenantProfile get(TenantProfileId tenantProfileId) {
TenantProfile profile = tenantProfilesMap.get(tenantProfileId);
if (profile == null) {
profile = tenantProfilesMap.get(tenantProfileId);
if (profile == null) {
tenantProfileFetchLock.lock();
try {
tenantProfileFetchLock.lock();
try {
profile = tenantProfilesMap.get(tenantProfileId);
if (profile == null) {
profile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, tenantProfileId);
if (profile != null) {
tenantProfilesMap.put(tenantProfileId, profile);
}
} finally {
tenantProfileFetchLock.unlock();
}
} finally {
tenantProfileFetchLock.unlock();
}
}
return profile;

6
application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java

@ -19,13 +19,17 @@ import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
public interface TbDeviceProfileCache extends RuleEngineDeviceProfileCache {
void put(DeviceProfile profile);
void evict(DeviceProfileId id);
void evict(TenantId tenantId, DeviceProfileId id);
void evict(DeviceId id);
DeviceProfile find(DeviceProfileId deviceProfileId);
DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String deviceType);
}

4
application/src/main/java/org/thingsboard/server/service/profile/TbTenantProfileCache.java

@ -15,11 +15,7 @@
*/
package org.thingsboard.server.service.profile;
import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;

59
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java

@ -23,11 +23,15 @@ import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
@ -189,21 +193,51 @@ public class DefaultTbClusterService implements TbClusterService {
@Override
public void onDeviceProfileChange(DeviceProfile deviceProfile, TbQueueCallback callback) {
log.trace("[{}][{}] Processing device profile [{}] change event", deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile.getName());
TransportProtos.DeviceProfileUpdateMsg profileUpdateMsg = TransportProtos.DeviceProfileUpdateMsg.newBuilder()
.setData(ByteString.copyFrom(encodingService.encode(deviceProfile))).build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setDeviceProfileUpdateMsg(profileUpdateMsg).build();
broadcast(transportMsg);
onEntityChange(deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile, callback);
}
@Override
public void onTenantProfileChange(TenantProfile tenantProfile, TbQueueCallback callback) {
onEntityChange(TenantId.SYS_TENANT_ID, tenantProfile.getId(), tenantProfile, callback);
}
@Override
public void onTenantChange(Tenant tenant, TbQueueCallback callback) {
onEntityChange(TenantId.SYS_TENANT_ID, tenant.getId(), tenant, callback);
}
@Override
public void onDeviceProfileDelete(DeviceProfile entity, TbQueueCallback callback) {
onEntityDelete(entity.getTenantId(), entity.getId(), entity.getName(), callback);
}
@Override
public void onDeviceProfileDelete(DeviceProfile deviceProfile, TbQueueCallback callback) {
log.trace("[{}][{}] Processing device profile [{}] delete event", deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile.getName());
TransportProtos.DeviceProfileDeleteMsg profileDeleteMsg = TransportProtos.DeviceProfileDeleteMsg.newBuilder()
.setProfileIdMSB(deviceProfile.getId().getId().getMostSignificantBits())
.setProfileIdLSB(deviceProfile.getId().getId().getLeastSignificantBits())
public void onTenantProfileDelete(TenantProfile entity, TbQueueCallback callback) {
onEntityDelete(TenantId.SYS_TENANT_ID, entity.getId(), entity.getName(), callback);
}
@Override
public void onTenantDelete(Tenant entity, TbQueueCallback callback) {
onEntityDelete(TenantId.SYS_TENANT_ID, entity.getId(), entity.getName(), callback);
}
public <T extends HasName> void onEntityChange(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) {
log.trace("[{}][{}][{}] Processing [{}] change event", tenantId, entityid.getEntityType(), entityid.getId(), entity.getName());
TransportProtos.EntityUpdateMsg entityUpdateMsg = TransportProtos.EntityUpdateMsg.newBuilder()
.setEntityType(entityid.getEntityType().name())
.setData(ByteString.copyFrom(encodingService.encode(entity))).build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setEntityUpdateMsg(entityUpdateMsg).build();
broadcast(transportMsg);
}
private void onEntityDelete(TenantId tenantId, EntityId entityId, String name, TbQueueCallback callback) {
log.trace("[{}][{}][{}] Processing [{}] delete event", tenantId, entityId.getEntityType(), entityId.getId(), name);
TransportProtos.EntityDeleteMsg entityDeleteMsg = TransportProtos.EntityDeleteMsg.newBuilder()
.setEntityType(entityId.getEntityType().name())
.setEntityIdMSB(entityId.getId().getMostSignificantBits())
.setEntityIdLSB(entityId.getId().getLeastSignificantBits())
.build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setDeviceProfileDeleteMsg(profileDeleteMsg).build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setEntityDeleteMsg(entityDeleteMsg).build();
broadcast(transportMsg);
}
@ -221,7 +255,8 @@ public class DefaultTbClusterService implements TbClusterService {
byte[] msgBytes = encodingService.encode(msg);
TbQueueProducer<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer();
Set<String> tbRuleEngineServices = new HashSet<>(partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE));
if (msg.getEntityId().getEntityType().equals(EntityType.TENANT)) {
if (msg.getEntityId().getEntityType().equals(EntityType.TENANT)
|| msg.getEntityId().getEntityType().equals(EntityType.DEVICE_PROFILE)) {
TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer();
Set<String> tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE);
for (String serviceId : tbCoreServices) {

13
application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java

@ -17,7 +17,8 @@ package org.thingsboard.server.service.queue;
import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@ -53,5 +54,13 @@ public interface TbClusterService {
void onDeviceProfileChange(DeviceProfile deviceProfile, TbQueueCallback callback);
void onDeviceProfileDelete(DeviceProfile deviceProfileId, TbQueueCallback callback);
void onDeviceProfileDelete(DeviceProfile deviceProfile, TbQueueCallback callback);
void onTenantProfileChange(TenantProfile tenantProfile, TbQueueCallback callback);
void onTenantProfileDelete(TenantProfile tenantProfile, TbQueueCallback callback);
void onTenantChange(Tenant tenant, TbQueueCallback callback);
void onTenantDelete(Tenant tenant, TbQueueCallback callback);
}

2
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -144,7 +144,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
if (actorMsg instanceof ComponentLifecycleMsg) {
ComponentLifecycleMsg componentLifecycleMsg = (ComponentLifecycleMsg) actorMsg;
if (EntityType.DEVICE_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
deviceProfileCache.evict(new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));
deviceProfileCache.evict(componentLifecycleMsg.getTenantId(), new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));
} else if (EntityType.DEVICE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
deviceProfileCache.evict(new DeviceId(componentLifecycleMsg.getEntityId().getId()));
}

22
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java

@ -31,6 +31,8 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.IdBased;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
@ -76,12 +78,15 @@ public abstract class AbstractOAuth2ClientMapper {
private final Lock userCreationLock = new ReentrantLock();
protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, boolean allowUserCreation, boolean activateUser) {
protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, OAuth2ClientRegistrationInfo clientRegistration) {
OAuth2MapperConfig config = clientRegistration.getMapperConfig();
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail());
User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail());
if (user == null && !allowUserCreation) {
if (user == null && !config.isAllowUserCreation()) {
throw new UsernameNotFoundException("User not found: " + oauth2User.getEmail());
}
@ -106,21 +111,28 @@ public abstract class AbstractOAuth2ClientMapper {
user.setFirstName(oauth2User.getFirstName());
user.setLastName(oauth2User.getLastName());
ObjectNode additionalInfo = objectMapper.createObjectNode();
if (!StringUtils.isEmpty(oauth2User.getDefaultDashboardName())) {
Optional<DashboardId> dashboardIdOpt =
user.getAuthority() == Authority.TENANT_ADMIN ?
getDashboardId(tenantId, oauth2User.getDefaultDashboardName())
: getDashboardId(tenantId, customerId, oauth2User.getDefaultDashboardName());
if (dashboardIdOpt.isPresent()) {
ObjectNode additionalInfo = objectMapper.createObjectNode();
additionalInfo.put("defaultDashboardFullscreen", oauth2User.isAlwaysFullScreen());
additionalInfo.put("defaultDashboardId", dashboardIdOpt.get().getId().toString());
user.setAdditionalInfo(additionalInfo);
}
}
if (clientRegistration.getAdditionalInfo() != null &&
clientRegistration.getAdditionalInfo().has("providerName")) {
additionalInfo.put("authProviderName", clientRegistration.getAdditionalInfo().get("providerName").asText());
}
user.setAdditionalInfo(additionalInfo);
user = userService.saveUser(user);
if (activateUser) {
if (config.isActivateUser()) {
UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId());
userService.activateUserCredentials(user.getTenantId(), userCredentials.getActivateToken(), passwordEncoder.encode(""));
}

6
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.security.auth.oauth2;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -29,11 +30,12 @@ import java.util.Map;
public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
@Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) {
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) {
OAuth2MapperConfig config = clientRegistration.getMapperConfig();
Map<String, Object> attributes = token.getPrincipal().getAttributes();
String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config);
return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser());
return getOrCreateSecurityUserFromOAuth2User(oauth2User, clientRegistration);
}
}

6
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java

@ -23,6 +23,7 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.dao.oauth2.OAuth2User;
@ -38,9 +39,10 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
@Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) {
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) {
OAuth2MapperConfig config = clientRegistration.getMapperConfig();
OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom());
return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser());
return getOrCreateSecurityUserFromOAuth2User(oauth2User, clientRegistration);
}
private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2CustomMapperConfig custom) {

6
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java

@ -23,6 +23,7 @@ import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.dao.oauth2.OAuth2User;
@ -45,12 +46,13 @@ public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
private OAuth2Configuration oAuth2Configuration;
@Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) {
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) {
OAuth2MapperConfig config = clientRegistration.getMapperConfig();
Map<String, String> githubMapperConfig = oAuth2Configuration.getGithubMapper();
String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken);
Map<String, Object> attributes = token.getPrincipal().getAttributes();
OAuth2User oAuth2User = BasicMapperUtils.getOAuth2User(email, attributes, config);
return getOrCreateSecurityUserFromOAuth2User(oAuth2User, config.isAllowUserCreation(), config.isActivateUser());
return getOrCreateSecurityUserFromOAuth2User(oAuth2User, clientRegistration);
}
private synchronized String getEmail(String emailUrl, String oauth2Token) {

4
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java

@ -16,9 +16,9 @@
package org.thingsboard.server.service.security.auth.oauth2;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface OAuth2ClientMapper {
SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config);
SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration);
}

4
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java

@ -74,7 +74,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
token.getPrincipal().getName());
OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(clientRegistration.getMapperConfig().getType());
SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(),
clientRegistration.getMapperConfig());
clientRegistration);
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
@ -85,4 +85,4 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8.toString()));
}
}
}
}

63
application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java

@ -28,6 +28,7 @@ import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.credentials.ProvisionDeviceCredentialsData;
@ -45,21 +46,19 @@ import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceProvisionService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.util.mapping.JacksonUtil;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto;
import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg;
@ -70,6 +69,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.profile.TbTenantProfileCache;
import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.state.DeviceStateService;
@ -90,9 +90,7 @@ public class DefaultTransportApiService implements TransportApiService {
private static final ObjectMapper mapper = new ObjectMapper();
//TODO: Constructor dependencies;
private final DeviceProfileService deviceProfileService;
private final TenantService tenantService;
private final TbDeviceProfileCache deviceProfileCache;
private final TbTenantProfileCache tenantProfileCache;
private final DeviceService deviceService;
private final RelationService relationService;
@ -103,17 +101,15 @@ public class DefaultTransportApiService implements TransportApiService {
private final DataDecodingEncodingService dataDecodingEncodingService;
private final DeviceProvisionService deviceProvisionService;
private final ConcurrentMap<String, ReentrantLock> deviceCreationLocks = new ConcurrentHashMap<>();
public DefaultTransportApiService(DeviceProfileService deviceProfileService, TenantService tenantService,
public DefaultTransportApiService(TbDeviceProfileCache deviceProfileCache,
TbTenantProfileCache tenantProfileCache, DeviceService deviceService,
RelationService relationService, DeviceCredentialsService deviceCredentialsService,
DeviceStateService deviceStateService, DbCallbackExecutorService dbCallbackExecutorService,
TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService,
DeviceProvisionService deviceProvisionService) {
this.deviceProfileService = deviceProfileService;
this.tenantService = tenantService;
this.deviceProfileCache = deviceProfileCache;
this.tenantProfileCache = tenantProfileCache;
this.deviceService = deviceService;
this.relationService = relationService;
@ -143,11 +139,8 @@ public class DefaultTransportApiService implements TransportApiService {
} else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) {
return Futures.transform(handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()),
value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
} else if (transportApiRequestMsg.hasGetTenantRoutingInfoRequestMsg()) {
return Futures.transform(handle(transportApiRequestMsg.getGetTenantRoutingInfoRequestMsg()),
value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
} else if (transportApiRequestMsg.hasGetDeviceProfileRequestMsg()) {
return Futures.transform(handle(transportApiRequestMsg.getGetDeviceProfileRequestMsg()),
} else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) {
return Futures.transform(handle(transportApiRequestMsg.getEntityProfileRequestMsg()),
value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
} else if (transportApiRequestMsg.hasProvisionDeviceRequestMsg()) {
return Futures.transform(handle(transportApiRequestMsg.getProvisionDeviceRequestMsg()),
@ -238,7 +231,7 @@ public class DefaultTransportApiService implements TransportApiService {
device.setName(requestMsg.getDeviceName());
device.setType(requestMsg.getDeviceType());
device.setCustomerId(gateway.getCustomerId());
DeviceProfile deviceProfile = deviceProfileService.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType());
DeviceProfile deviceProfile = deviceProfileCache.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType());
device.setDeviceProfileId(deviceProfile.getId());
device = deviceService.saveDevice(device);
relationService.saveRelationAsync(TenantId.SYS_TENANT_ID, new EntityRelation(gateway.getId(), device.getId(), "Created"));
@ -258,7 +251,7 @@ public class DefaultTransportApiService implements TransportApiService {
}
GetOrCreateDeviceFromGatewayResponseMsg.Builder builder = GetOrCreateDeviceFromGatewayResponseMsg.newBuilder()
.setDeviceInfo(getDeviceInfoProto(device));
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId());
DeviceProfile deviceProfile = deviceProfileCache.get(device.getTenantId(), device.getDeviceProfileId());
if (deviceProfile != null) {
builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile)));
} else {
@ -320,23 +313,21 @@ public class DefaultTransportApiService implements TransportApiService {
.build();
}
private ListenableFuture<TransportApiResponseMsg> handle(GetTenantRoutingInfoRequestMsg requestMsg) {
TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB()));
ListenableFuture<TenantProfile> tenantProfileFuture = Futures.immediateFuture(tenantProfileCache.get(tenantId));
return Futures.transform(tenantProfileFuture, tenantProfile -> TransportApiResponseMsg.newBuilder()
.setGetTenantRoutingInfoResponseMsg(GetTenantRoutingInfoResponseMsg.newBuilder().setIsolatedTbCore(tenantProfile.isIsolatedTbCore())
.setIsolatedTbRuleEngine(tenantProfile.isIsolatedTbRuleEngine()).build()).build(), dbCallbackExecutorService);
}
private ListenableFuture<TransportApiResponseMsg> handle(TransportProtos.GetDeviceProfileRequestMsg requestMsg) {
DeviceProfileId profileId = new DeviceProfileId(new UUID(requestMsg.getProfileIdMSB(), requestMsg.getProfileIdLSB()));
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(TenantId.SYS_TENANT_ID, profileId);
return Futures.immediateFuture(TransportApiResponseMsg.newBuilder()
.setGetDeviceProfileResponseMsg(
TransportProtos.GetDeviceProfileResponseMsg.newBuilder()
.setData(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile)))
.build()).build());
private ListenableFuture<TransportApiResponseMsg> handle(GetEntityProfileRequestMsg requestMsg) {
EntityType entityType = EntityType.valueOf(requestMsg.getEntityType());
UUID entityUuid = new UUID(requestMsg.getEntityIdMSB(), requestMsg.getEntityIdLSB());
ByteString data;
if (entityType.equals(EntityType.DEVICE_PROFILE)) {
DeviceProfileId deviceProfileId = new DeviceProfileId(entityUuid);
DeviceProfile deviceProfile = deviceProfileCache.find(deviceProfileId);
data = ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile));
} else if (entityType.equals(EntityType.TENANT)) {
TenantProfile tenantProfile = tenantProfileCache.get(new TenantId(entityUuid));
data = ByteString.copyFrom(dataDecodingEncodingService.encode(tenantProfile));
} else {
throw new RuntimeException("Invalid entity profile request: " + entityType);
}
return Futures.immediateFuture(TransportApiResponseMsg.newBuilder().setEntityProfileResponseMsg(GetEntityProfileResponseMsg.newBuilder().setData(data).build()).build());
}
private ListenableFuture<TransportApiResponseMsg> getDeviceInfo(DeviceId deviceId, DeviceCredentials credentials) {
@ -348,7 +339,7 @@ public class DefaultTransportApiService implements TransportApiService {
try {
ValidateDeviceCredentialsResponseMsg.Builder builder = ValidateDeviceCredentialsResponseMsg.newBuilder();
builder.setDeviceInfo(getDeviceInfoProto(device));
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId());
DeviceProfile deviceProfile = deviceProfileCache.get(device.getTenantId(), device.getDeviceProfileId());
if (deviceProfile != null) {
builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile)));
} else {

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

@ -502,7 +502,7 @@ js:
remote:
# Maximum allowed JavaScript execution errors before JavaScript will be blacklisted
max_errors: "${REMOTE_JS_SANDBOX_MAX_ERRORS:3}"
# Maximum time in seconds for black listed function to stay in the list.
# Maximum time in seconds for black listed function to stay in 1:the list.
max_black_list_duration_sec: "${REMOTE_JS_SANDBOX_MAX_BLACKLIST_DURATION_SEC:60}"
stats:
enabled: "${TB_JS_REMOTE_STATS_ENABLED:false}"
@ -512,10 +512,6 @@ transport:
sessions:
inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}"
report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}"
rate_limits:
enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}"
tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}"
device: "${TB_TRANSPORT_RATE_LIMITS_DEVICE:10:1,300:60}"
json:
# Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON
type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}"

6
common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsDomainParams.java

@ -27,6 +27,6 @@ import java.util.Set;
@NoArgsConstructor
@AllArgsConstructor
public class OAuth2ClientsDomainParams {
private Set<DomainInfo> domainInfos;
private Set<ClientRegistrationDto> clientRegistrations;
}
private List<DomainInfo> domainInfos;
private List<ClientRegistrationDto> clientRegistrations;
}

6
common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsParams.java

@ -16,6 +16,8 @@
package org.thingsboard.server.common.data.oauth2;
import lombok.*;
import java.util.List;
import java.util.Set;
@EqualsAndHashCode
@ -26,5 +28,5 @@ import java.util.Set;
@AllArgsConstructor
public class OAuth2ClientsParams {
private boolean enabled;
private Set<OAuth2ClientsDomainParams> domainsParams;
}
private List<OAuth2ClientsDomainParams> domainsParams;
}

4
common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java

@ -50,4 +50,8 @@ public class TbRateLimits {
return bucket.tryConsume(1);
}
public boolean tryConsume(long number) {
return bucket.tryConsume(number);
}
}

26
common/queue/src/main/java/org/thingsboard/server/queue/util/TbTransportComponent.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2020 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.queue.util;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
@ConditionalOnExpression("('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport'")
public @interface TbTransportComponent {
}

47
common/queue/src/main/proto/queue.proto

@ -177,32 +177,26 @@ message GetOrCreateDeviceFromGatewayResponseMsg {
bytes profileBody = 2;
}
message GetTenantRoutingInfoRequestMsg {
int64 tenantIdMSB = 1;
int64 tenantIdLSB = 2;
}
message GetTenantRoutingInfoResponseMsg {
bool isolatedTbCore = 1;
bool isolatedTbRuleEngine = 2;
}
message GetDeviceProfileRequestMsg {
int64 profileIdMSB = 1;
int64 profileIdLSB = 2;
message GetEntityProfileRequestMsg {
string entityType = 1;
int64 entityIdMSB = 2;
int64 entityIdLSB = 3;
}
message GetDeviceProfileResponseMsg {
bytes data = 1;
message GetEntityProfileResponseMsg {
string entityType = 1;
bytes data = 2;
}
message DeviceProfileUpdateMsg {
bytes data = 1;
message EntityUpdateMsg {
string entityType = 1;
bytes data = 2;
}
message DeviceProfileDeleteMsg {
int64 profileIdMSB = 1;
int64 profileIdLSB = 2;
message EntityDeleteMsg {
string entityType = 1;
int64 entityIdMSB = 2;
int64 entityIdLSB = 3;
}
message SessionCloseNotificationProto {
@ -482,8 +476,7 @@ message TransportApiRequestMsg {
ValidateDeviceTokenRequestMsg validateTokenRequestMsg = 1;
ValidateDeviceX509CertRequestMsg validateX509CertRequestMsg = 2;
GetOrCreateDeviceFromGatewayRequestMsg getOrCreateDeviceRequestMsg = 3;
GetTenantRoutingInfoRequestMsg getTenantRoutingInfoRequestMsg = 4;
GetDeviceProfileRequestMsg getDeviceProfileRequestMsg = 5;
GetEntityProfileRequestMsg entityProfileRequestMsg = 4;
ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 6;
ProvisionDeviceRequestMsg provisionDeviceRequestMsg = 7;
}
@ -492,9 +485,8 @@ message TransportApiRequestMsg {
message TransportApiResponseMsg {
ValidateDeviceCredentialsResponseMsg validateCredResponseMsg = 1;
GetOrCreateDeviceFromGatewayResponseMsg getOrCreateDeviceResponseMsg = 2;
GetTenantRoutingInfoResponseMsg getTenantRoutingInfoResponseMsg = 4;
GetDeviceProfileResponseMsg getDeviceProfileResponseMsg = 5;
ProvisionDeviceResponseMsg provisionDeviceResponseMsg = 6;
GetEntityProfileResponseMsg entityProfileResponseMsg = 3;
ProvisionDeviceResponseMsg provisionDeviceResponseMsg = 4;
}
/* Messages that are handled by ThingsBoard Core Service */
@ -535,7 +527,8 @@ message ToTransportMsg {
AttributeUpdateNotificationMsg attributeUpdateNotification = 5;
ToDeviceRpcRequestMsg toDeviceRequest = 6;
ToServerRpcResponseMsg toServerResponse = 7;
DeviceProfileUpdateMsg deviceProfileUpdateMsg = 8;
DeviceProfileDeleteMsg deviceProfileDeleteMsg = 9;
/* For Tenant, TenantProfile and DeviceProfile */
EntityUpdateMsg entityUpdateMsg = 8;
EntityDeleteMsg entityDeleteMsg = 9;
ProvisionDeviceResponseMsg provisionResponse = 10;
}

8
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -263,12 +263,12 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
private void processDevicePublish(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg, String topicName, int msgId) {
try {
MqttTransportAdaptor payloadAdaptor = deviceSessionCtx.getPayloadAdaptor();
if (deviceSessionCtx.isDeviceTelemetryTopic(topicName)) {
TransportProtos.PostTelemetryMsg postTelemetryMsg = payloadAdaptor.convertToPostTelemetry(deviceSessionCtx, mqttMsg);
transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg));
} else if (deviceSessionCtx.isDeviceAttributesTopic(topicName)) {
if (deviceSessionCtx.isDeviceAttributesTopic(topicName)) {
TransportProtos.PostAttributeMsg postAttributeMsg = payloadAdaptor.convertToPostAttributes(deviceSessionCtx, mqttMsg);
transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(ctx, msgId, postAttributeMsg));
} else if (deviceSessionCtx.isDeviceTelemetryTopic(topicName)) {
TransportProtos.PostTelemetryMsg postTelemetryMsg = payloadAdaptor.convertToPostTelemetry(deviceSessionCtx, mqttMsg);
transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg));
} else if (topicName.startsWith(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX)) {
TransportProtos.GetAttributeRequestMsg getAttributeMsg = payloadAdaptor.convertToGetAttributes(deviceSessionCtx, mqttMsg);
transportService.process(deviceSessionCtx.getSessionInfo(), getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg));

2
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportProfileCache.java → common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportDeviceProfileCache.java

@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import java.util.Optional;
public interface TransportProfileCache {
public interface TransportDeviceProfileCache {
DeviceProfile getOrCreate(DeviceProfileId id, ByteString profileBody);

12
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java

@ -20,11 +20,13 @@ import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.common.transport.limits.TransportRateLimitType;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg;
import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
@ -47,7 +49,7 @@ import java.util.concurrent.ScheduledExecutorService;
*/
public interface TransportService {
GetTenantRoutingInfoResponseMsg getRoutingInfo(GetTenantRoutingInfoRequestMsg msg);
GetEntityProfileResponseMsg getRoutingInfo(GetEntityProfileRequestMsg msg);
void process(DeviceTransportType transportType, ValidateDeviceTokenRequestMsg msg,
TransportServiceCallback<ValidateDeviceCredentialsResponse> callback);
@ -64,12 +66,12 @@ public interface TransportService {
void process(ProvisionDeviceRequestMsg msg,
TransportServiceCallback<ProvisionDeviceResponseMsg> callback);
void getDeviceProfile(DeviceProfileId deviceProfileId, TransportServiceCallback<DeviceProfile> callback);
void onProfileUpdate(DeviceProfile deviceProfile);
boolean checkLimits(SessionInfoProto sessionInfo, Object msg, TransportServiceCallback<Void> callback);
boolean checkLimits(SessionInfoProto sessionInfo, Object msg, TransportServiceCallback<Void> callback, int dataPoints, TransportRateLimitType... limits);
void process(SessionInfoProto sessionInfo, SessionEventMsg msg, TransportServiceCallback<Void> callback);
void process(SessionInfoProto sessionInfo, PostTelemetryMsg msg, TransportServiceCallback<Void> callback);

38
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportTenantProfileCache.java

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.transport;
import com.google.protobuf.ByteString;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult;
import org.thingsboard.server.queue.discovery.TenantRoutingInfo;
import org.thingsboard.server.queue.discovery.TenantRoutingInfoService;
import java.util.Set;
public interface TransportTenantProfileCache {
TenantProfile get(TenantId tenantId);
TenantProfileUpdateResult put(ByteString profileBody);
boolean put(TenantId tenantId, TenantProfileId profileId);
Set<TenantId> remove(TenantProfileId profileId);
}

47
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitFactory.java

@ -0,0 +1,47 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.transport.limits;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
@Slf4j
@Component
public class DefaultTransportRateLimitFactory implements TransportRateLimitFactory {
private static final DummyTransportRateLimit ALWAYS_TRUE = new DummyTransportRateLimit();
@Override
public TransportRateLimit create(TransportRateLimitType type, Object configuration) {
if (!StringUtils.isEmpty(configuration)) {
try {
return new SimpleTransportRateLimit(new TbRateLimits(configuration.toString()), configuration.toString());
} catch (Exception e) {
log.warn("[{}] Failed to init rate limit with configuration: {}", type, configuration, e);
return ALWAYS_TRUE;
}
} else {
return ALWAYS_TRUE;
}
}
@Override
public TransportRateLimit createDefault(TransportRateLimitType type) {
return ALWAYS_TRUE;
}
}

133
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java

@ -0,0 +1,133 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.transport.limits;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.TenantProfileData;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.transport.TransportTenantProfileCache;
import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult;
import org.thingsboard.server.queue.util.TbTransportComponent;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Service
@TbTransportComponent
@Slf4j
public class DefaultTransportRateLimitService implements TransportRateLimitService {
private final ConcurrentMap<TenantId, TransportRateLimit[]> perTenantLimits = new ConcurrentHashMap<>();
private final ConcurrentMap<DeviceId, TransportRateLimit[]> perDeviceLimits = new ConcurrentHashMap<>();
private final TransportRateLimitFactory rateLimitFactory;
private final TransportTenantProfileCache tenantProfileCache;
public DefaultTransportRateLimitService(TransportRateLimitFactory rateLimitFactory, TransportTenantProfileCache tenantProfileCache) {
this.rateLimitFactory = rateLimitFactory;
this.tenantProfileCache = tenantProfileCache;
}
@Override
public TransportRateLimitType checkLimits(TenantId tenantId, DeviceId deviceId, int dataPoints, TransportRateLimitType... limits) {
TransportRateLimit[] tenantLimits = getTenantRateLimits(tenantId);
TransportRateLimit[] deviceLimits = getDeviceRateLimits(tenantId, deviceId);
for (TransportRateLimitType limitType : limits) {
TransportRateLimit rateLimit;
if (limitType.isTenantLevel()) {
rateLimit = tenantLimits[limitType.ordinal()];
} else {
rateLimit = deviceLimits[limitType.ordinal()];
}
if (!rateLimit.tryConsume(limitType.isMessageLevel() ? 1L : dataPoints)) {
return limitType;
}
}
return null;
}
@Override
public void update(TenantProfileUpdateResult update) {
TransportRateLimit[] newLimits = createTransportRateLimits(update.getProfile());
for (TenantId tenantId : update.getAffectedTenants()) {
mergeLimits(tenantId, newLimits);
}
}
@Override
public void update(TenantId tenantId) {
mergeLimits(tenantId, fetchProfileAndInit(tenantId));
}
@Override
public void remove(TenantId tenantId) {
perTenantLimits.remove(tenantId);
}
@Override
public void remove(DeviceId deviceId) {
perDeviceLimits.remove(deviceId);
}
private void mergeLimits(TenantId tenantId, TransportRateLimit[] newRateLimits) {
TransportRateLimit[] oldRateLimits = perTenantLimits.get(tenantId);
if (oldRateLimits == null) {
perTenantLimits.put(tenantId, newRateLimits);
} else {
for (int i = 0; i < TransportRateLimitType.values().length; i++) {
TransportRateLimit newLimit = newRateLimits[i];
TransportRateLimit oldLimit = oldRateLimits[i];
if (newLimit != null && (oldLimit == null || !oldLimit.getConfiguration().equals(newLimit.getConfiguration()))) {
oldRateLimits[i] = newLimit;
}
}
}
}
private TransportRateLimit[] fetchProfileAndInit(TenantId tenantId) {
return perTenantLimits.computeIfAbsent(tenantId, tmp -> createTransportRateLimits(tenantProfileCache.get(tenantId)));
}
private TransportRateLimit[] createTransportRateLimits(TenantProfile tenantProfile) {
TenantProfileData profileData = tenantProfile.getProfileData();
TransportRateLimit[] rateLimits = new TransportRateLimit[TransportRateLimitType.values().length];
for (TransportRateLimitType type : TransportRateLimitType.values()) {
rateLimits[type.ordinal()] = rateLimitFactory.create(type, profileData.getProperties().get(type.getConfigurationKey()));
}
return rateLimits;
}
private TransportRateLimit[] getTenantRateLimits(TenantId tenantId) {
TransportRateLimit[] limits = perTenantLimits.get(tenantId);
if (limits == null) {
limits = fetchProfileAndInit(tenantId);
perTenantLimits.put(tenantId, limits);
}
return limits;
}
private TransportRateLimit[] getDeviceRateLimits(TenantId tenantId, DeviceId deviceId) {
TransportRateLimit[] limits = perDeviceLimits.get(deviceId);
if (limits == null) {
limits = fetchProfileAndInit(tenantId);
perDeviceLimits.put(deviceId, limits);
}
return limits;
}
}

35
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DummyTransportRateLimit.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.transport.limits;
public class DummyTransportRateLimit implements TransportRateLimit {
@Override
public String getConfiguration() {
return "";
}
@Override
public boolean tryConsume(long number) {
return true;
}
@Override
public boolean tryConsume() {
return true;
}
}

38
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/SimpleTransportRateLimit.java

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.transport.limits;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
@RequiredArgsConstructor
public class SimpleTransportRateLimit implements TransportRateLimit {
private final TbRateLimits rateLimit;
@Getter
private final String configuration;
@Override
public boolean tryConsume() {
return rateLimit.tryConsume();
}
@Override
public boolean tryConsume(long number) {
return number <= 0 || rateLimit.tryConsume(number);
}
}

26
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimit.java

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

24
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitFactory.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.transport.limits;
public interface TransportRateLimitFactory {
TransportRateLimit create(TransportRateLimitType type, Object config);
TransportRateLimit createDefault(TransportRateLimitType type);
}

34
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.transport.limits;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult;
public interface TransportRateLimitService {
TransportRateLimitType checkLimits(TenantId tenantId, DeviceId deviceId, int dataPoints, TransportRateLimitType... limits);
void update(TenantProfileUpdateResult update);
void update(TenantId tenantId);
void remove(TenantId tenantId);
void remove(DeviceId deviceId);
}

47
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitType.java

@ -0,0 +1,47 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.transport.limits;
import lombok.Getter;
public enum TransportRateLimitType {
TENANT_MAX_MSGS("transport.tenant.msg", true, true),
TENANT_TELEMETRY_MSGS("transport.tenant.telemetry", true, true),
TENANT_MAX_DATA_POINTS("transport.tenant.dataPoints", true, false),
DEVICE_MAX_MSGS("transport.device.msg", false, true),
DEVICE_TELEMETRY_MSGS("transport.device.telemetry", false, true),
DEVICE_MAX_DATA_POINTS("transport.device.dataPoints", false, false);
@Getter
private final String configurationKey;
@Getter
private final boolean tenantLevel;
@Getter
private final boolean deviceLevel;
@Getter
private final boolean messageLevel;
@Getter
private final boolean dataPointLevel;
TransportRateLimitType(String configurationKey, boolean tenantLevel, boolean messageLevel) {
this.configurationKey = configurationKey;
this.tenantLevel = tenantLevel;
this.deviceLevel = !tenantLevel;
this.messageLevel = messageLevel;
this.dataPointLevel = !messageLevel;
}
}

30
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/profile/TenantProfileUpdateResult.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.transport.profile;
import lombok.Data;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.Set;
@Data
public class TenantProfileUpdateResult {
private final TenantProfile profile;
private final Set<TenantId> affectedTenants;
}

9
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportProfileCache.java → common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportDeviceProfileCache.java

@ -21,8 +21,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.transport.TransportProfileCache;
import org.thingsboard.server.common.transport.TransportDeviceProfileCache;
import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
import org.thingsboard.server.queue.util.TbTransportComponent;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@ -30,14 +31,14 @@ import java.util.concurrent.ConcurrentMap;
@Slf4j
@Component
@ConditionalOnExpression("('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport'")
public class DefaultTransportProfileCache implements TransportProfileCache {
@TbTransportComponent
public class DefaultTransportDeviceProfileCache implements TransportDeviceProfileCache {
private final ConcurrentMap<DeviceProfileId, DeviceProfile> deviceProfiles = new ConcurrentHashMap<>();
private final DataDecodingEncodingService dataDecodingEncodingService;
public DefaultTransportProfileCache(DataDecodingEncodingService dataDecodingEncodingService) {
public DefaultTransportDeviceProfileCache(DataDecodingEncodingService dataDecodingEncodingService) {
this.dataDecodingEncodingService = dataDecodingEncodingService;
}

192
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -29,25 +29,35 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.ServiceQueue;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.session.SessionMsgType;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.common.stats.MessagesStats;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.StatsType;
import org.thingsboard.server.common.transport.SessionMsgListener;
import org.thingsboard.server.common.transport.TransportProfileCache;
import org.thingsboard.server.common.transport.TransportDeviceProfileCache;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.TransportTenantProfileCache;
import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse;
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.common.transport.limits.TransportRateLimit;
import org.thingsboard.server.common.transport.limits.TransportRateLimitService;
import org.thingsboard.server.common.transport.limits.TransportRateLimitType;
import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult;
import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
import org.thingsboard.server.common.transport.util.JsonUtils;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
@ -69,15 +79,14 @@ import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.provider.TbTransportQueueFactory;
import org.thingsboard.server.common.stats.MessagesStats;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.StatsType;
import org.thingsboard.server.queue.util.TbTransportComponent;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@ -95,15 +104,9 @@ import java.util.concurrent.atomic.AtomicInteger;
*/
@Slf4j
@Service
@ConditionalOnExpression("('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport'")
@TbTransportComponent
public class DefaultTransportService implements TransportService {
@Value("${transport.rate_limits.enabled}")
private boolean rateLimitEnabled;
@Value("${transport.rate_limits.tenant}")
private String perTenantLimitsConf;
@Value("${transport.rate_limits.device}")
private String perDevicesLimitsConf;
@Value("${transport.sessions.inactivity_timeout}")
private long sessionInactivityTimeout;
@Value("${transport.sessions.report_timeout}")
@ -119,7 +122,10 @@ public class DefaultTransportService implements TransportService {
private final PartitionService partitionService;
private final TbServiceInfoProvider serviceInfoProvider;
private final StatsFactory statsFactory;
private final TransportProfileCache transportProfileCache;
private final TransportDeviceProfileCache deviceProfileCache;
private final TransportTenantProfileCache tenantProfileCache;
private final TransportRateLimitService rateLimitService;
private final DataDecodingEncodingService dataDecodingEncodingService;
protected TbQueueRequestTemplate<TbProtoQueueMsg<TransportApiRequestMsg>, TbProtoQueueMsg<TransportApiResponseMsg>> transportApiRequestTemplate;
protected TbQueueProducer<TbProtoQueueMsg<ToRuleEngineMsg>> ruleEngineMsgProducer;
@ -132,14 +138,11 @@ public class DefaultTransportService implements TransportService {
protected ScheduledExecutorService schedulerExecutor;
protected ExecutorService transportCallbackExecutor;
private ExecutorService mainConsumerExecutor;
private final ConcurrentMap<UUID, SessionMetaData> sessions = new ConcurrentHashMap<>();
private final Map<String, RpcRequestMetadata> toServerRpcPendingMap = new ConcurrentHashMap<>();
//TODO 3.2: @ybondarenko Implement cleanup of this maps.
private final ConcurrentMap<TenantId, TbRateLimits> perTenantLimits = new ConcurrentHashMap<>();
private final ConcurrentMap<DeviceId, TbRateLimits> perDeviceLimits = new ConcurrentHashMap<>();
private ExecutorService mainConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer"));
private volatile boolean stopped = false;
public DefaultTransportService(TbServiceInfoProvider serviceInfoProvider,
@ -147,22 +150,22 @@ public class DefaultTransportService implements TransportService {
TbQueueProducerProvider producerProvider,
PartitionService partitionService,
StatsFactory statsFactory,
TransportProfileCache transportProfileCache) {
TransportDeviceProfileCache deviceProfileCache,
TransportTenantProfileCache tenantProfileCache,
TransportRateLimitService rateLimitService, DataDecodingEncodingService dataDecodingEncodingService) {
this.serviceInfoProvider = serviceInfoProvider;
this.queueProvider = queueProvider;
this.producerProvider = producerProvider;
this.partitionService = partitionService;
this.statsFactory = statsFactory;
this.transportProfileCache = transportProfileCache;
this.deviceProfileCache = deviceProfileCache;
this.tenantProfileCache = tenantProfileCache;
this.rateLimitService = rateLimitService;
this.dataDecodingEncodingService = dataDecodingEncodingService;
}
@PostConstruct
public void init() {
if (rateLimitEnabled) {
//Just checking the configuration parameters
new TbRateLimits(perTenantLimitsConf);
new TbRateLimits(perDevicesLimitsConf);
}
this.ruleEngineProducerStats = statsFactory.createMessagesStats(StatsType.RULE_ENGINE.getName() + ".producer");
this.tbCoreProducerStats = statsFactory.createMessagesStats(StatsType.CORE.getName() + ".producer");
this.transportApiStats = statsFactory.createMessagesStats(StatsType.TRANSPORT.getName() + ".producer");
@ -177,6 +180,7 @@ public class DefaultTransportService implements TransportService {
TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceInfoProvider.getServiceId());
transportNotificationsConsumer.subscribe(Collections.singleton(tpi));
transportApiRequestTemplate.init();
mainConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer"));
mainConsumerExecutor.execute(() -> {
while (!stopped) {
try {
@ -208,10 +212,6 @@ public class DefaultTransportService implements TransportService {
@PreDestroy
public void destroy() {
if (rateLimitEnabled) {
perTenantLimits.clear();
perDeviceLimits.clear();
}
stopped = true;
if (transportNotificationsConsumer != null) {
@ -232,7 +232,7 @@ public class DefaultTransportService implements TransportService {
}
@Override
public ScheduledExecutorService getSchedulerExecutor(){
public ScheduledExecutorService getSchedulerExecutor() {
return this.schedulerExecutor;
}
@ -242,12 +242,12 @@ public class DefaultTransportService implements TransportService {
}
@Override
public TransportProtos.GetTenantRoutingInfoResponseMsg getRoutingInfo(TransportProtos.GetTenantRoutingInfoRequestMsg msg) {
public TransportProtos.GetEntityProfileResponseMsg getRoutingInfo(TransportProtos.GetEntityProfileRequestMsg msg) {
TbProtoQueueMsg<TransportProtos.TransportApiRequestMsg> protoMsg =
new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setGetTenantRoutingInfoRequestMsg(msg).build());
new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setEntityProfileRequestMsg(msg).build());
try {
TbProtoQueueMsg<TransportApiResponseMsg> response = transportApiRequestTemplate.send(protoMsg).get();
return response.getValue().getGetTenantRoutingInfoResponseMsg();
return response.getValue().getEntityProfileResponseMsg();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
@ -289,7 +289,7 @@ public class DefaultTransportService implements TransportService {
result.deviceInfo(tdi);
ByteString profileBody = msg.getProfileBody();
if (profileBody != null && !profileBody.isEmpty()) {
DeviceProfile profile = transportProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody);
DeviceProfile profile = deviceProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody);
if (transportType != DeviceTransportType.DEFAULT
&& profile != null && profile.getTransportType() != DeviceTransportType.DEFAULT && profile.getTransportType() != transportType) {
log.debug("[{}] Device profile [{}] has different transport type: {}, expected: {}", tdi.getDeviceId(), tdi.getDeviceProfileId(), profile.getTransportType(), transportType);
@ -315,7 +315,7 @@ public class DefaultTransportService implements TransportService {
result.deviceInfo(tdi);
ByteString profileBody = msg.getProfileBody();
if (profileBody != null && !profileBody.isEmpty()) {
result.deviceProfile(transportProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody));
result.deviceProfile(deviceProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody));
}
}
return result.build();
@ -339,8 +339,8 @@ public class DefaultTransportService implements TransportService {
log.trace("Processing msg: {}", requestMsg);
TbProtoQueueMsg<TransportApiRequestMsg> protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setProvisionDeviceRequestMsg(requestMsg).build());
ListenableFuture<ProvisionDeviceResponseMsg> response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp ->
tmp.getValue().getProvisionDeviceResponseMsg()
, MoreExecutors.directExecutor());
tmp.getValue().getProvisionDeviceResponseMsg()
, MoreExecutors.directExecutor());
AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor);
}
@ -364,7 +364,11 @@ public class DefaultTransportService implements TransportService {
@Override
public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.PostTelemetryMsg msg, TransportServiceCallback<Void> callback) {
if (checkLimits(sessionInfo, msg, callback)) {
int dataPoints = 0;
for (TransportProtos.TsKvListProto tsKv : msg.getTsKvListList()) {
dataPoints += tsKv.getKvCount();
}
if (checkLimits(sessionInfo, msg, callback, dataPoints, TELEMETRY)) {
reportActivityInternal(sessionInfo);
TenantId tenantId = new TenantId(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB()));
DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB()));
@ -385,7 +389,7 @@ public class DefaultTransportService implements TransportService {
@Override
public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.PostAttributeMsg msg, TransportServiceCallback<Void> callback) {
if (checkLimits(sessionInfo, msg, callback)) {
if (checkLimits(sessionInfo, msg, callback, msg.getKvCount(), TELEMETRY)) {
reportActivityInternal(sessionInfo);
TenantId tenantId = new TenantId(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB()));
DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB()));
@ -575,38 +579,34 @@ public class DefaultTransportService implements TransportService {
sessions.remove(toSessionId(sessionInfo));
}
private TransportRateLimitType[] DEFAULT = new TransportRateLimitType[]{TransportRateLimitType.TENANT_MAX_MSGS, TransportRateLimitType.DEVICE_MAX_MSGS};
private TransportRateLimitType[] TELEMETRY = TransportRateLimitType.values();
@Override
public boolean checkLimits(TransportProtos.SessionInfoProto sessionInfo, Object msg, TransportServiceCallback<Void> callback) {
return checkLimits(sessionInfo, msg, callback, 0, DEFAULT);
}
@Override
public boolean checkLimits(TransportProtos.SessionInfoProto sessionInfo, Object msg, TransportServiceCallback<Void> callback, int dataPoints, TransportRateLimitType... limits) {
if (log.isTraceEnabled()) {
log.trace("[{}] Processing msg: {}", toSessionId(sessionInfo), msg);
}
if (!rateLimitEnabled) {
return true;
}
TenantId tenantId = new TenantId(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB()));
TbRateLimits rateLimits = perTenantLimits.computeIfAbsent(tenantId, id -> new TbRateLimits(perTenantLimitsConf));
if (!rateLimits.tryConsume()) {
if (callback != null) {
callback.onError(new TbRateLimitsException(EntityType.TENANT));
}
if (log.isTraceEnabled()) {
log.trace("[{}][{}] Tenant level rate limit detected: {}", toSessionId(sessionInfo), tenantId, msg);
}
return false;
}
DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB()));
rateLimits = perDeviceLimits.computeIfAbsent(deviceId, id -> new TbRateLimits(perDevicesLimitsConf));
if (!rateLimits.tryConsume()) {
TransportRateLimitType limit = rateLimitService.checkLimits(tenantId, deviceId, 0, limits);
if (limit == null) {
return true;
} else {
if (callback != null) {
callback.onError(new TbRateLimitsException(EntityType.DEVICE));
callback.onError(new TbRateLimitsException(limit.isTenantLevel() ? EntityType.TENANT : EntityType.DEVICE));
}
if (log.isTraceEnabled()) {
log.trace("[{}][{}] Device level rate limit detected: {}", toSessionId(sessionInfo), deviceId, msg);
log.trace("[{}][{}] {} rateLimit detected: {}", toSessionId(sessionInfo), tenantId, limit, msg);
}
return false;
}
return true;
}
protected void processToTransportMsg(TransportProtos.ToTransportMsg toSessionMsg) {
@ -637,16 +637,40 @@ public class DefaultTransportService implements TransportService {
deregisterSession(md.getSessionInfo());
}
} else {
if (toSessionMsg.hasDeviceProfileUpdateMsg()) {
DeviceProfile deviceProfile = transportProfileCache.put(toSessionMsg.getDeviceProfileUpdateMsg().getData());
if (deviceProfile != null) {
onProfileUpdate(deviceProfile);
if (toSessionMsg.hasEntityUpdateMsg()) {
TransportProtos.EntityUpdateMsg msg = toSessionMsg.getEntityUpdateMsg();
EntityType entityType = EntityType.valueOf(msg.getEntityType());
if (EntityType.DEVICE_PROFILE.equals(entityType)) {
DeviceProfile deviceProfile = deviceProfileCache.put(msg.getData());
if (deviceProfile != null) {
onProfileUpdate(deviceProfile);
}
} else if (EntityType.TENANT_PROFILE.equals(entityType)) {
TenantProfileUpdateResult update = tenantProfileCache.put(msg.getData());
rateLimitService.update(update);
} else if (EntityType.TENANT.equals(entityType)) {
Optional<Tenant> profileOpt = dataDecodingEncodingService.decode(msg.getData().toByteArray());
if (profileOpt.isPresent()) {
Tenant tenant = profileOpt.get();
boolean updated = tenantProfileCache.put(tenant.getId(), tenant.getTenantProfileId());
if (updated) {
rateLimitService.update(tenant.getId());
}
}
}
} else if (toSessionMsg.hasEntityDeleteMsg()) {
TransportProtos.EntityDeleteMsg msg = toSessionMsg.getEntityDeleteMsg();
EntityType entityType = EntityType.valueOf(msg.getEntityType());
UUID entityUuid = new UUID(msg.getEntityIdMSB(), msg.getEntityIdLSB());
if (EntityType.DEVICE_PROFILE.equals(entityType)) {
deviceProfileCache.evict(new DeviceProfileId(new UUID(msg.getEntityIdMSB(), msg.getEntityIdLSB())));
} else if (EntityType.TENANT_PROFILE.equals(entityType)) {
tenantProfileCache.remove(new TenantProfileId(entityUuid));
} else if (EntityType.TENANT.equals(entityType)) {
rateLimitService.remove(new TenantId(entityUuid));
} else if (EntityType.DEVICE.equals(entityType)) {
rateLimitService.remove(new DeviceId(entityUuid));
}
} else if (toSessionMsg.hasDeviceProfileDeleteMsg()) {
transportProfileCache.evict(new DeviceProfileId(new UUID(
toSessionMsg.getDeviceProfileDeleteMsg().getProfileIdMSB(),
toSessionMsg.getDeviceProfileDeleteMsg().getProfileIdLSB()
)));
} else {
//TODO: should we notify the device actor about missed session?
log.debug("[{}] Missing session.", sessionId);
@ -654,38 +678,6 @@ public class DefaultTransportService implements TransportService {
}
}
@Override
public void getDeviceProfile(DeviceProfileId deviceProfileId, TransportServiceCallback<DeviceProfile> callback) {
DeviceProfile deviceProfile = transportProfileCache.get(deviceProfileId);
if (deviceProfile != null) {
callback.onSuccess(deviceProfile);
} else {
log.trace("Processing device profile request: [{}]", deviceProfileId);
TransportProtos.GetDeviceProfileRequestMsg msg = TransportProtos.GetDeviceProfileRequestMsg.newBuilder()
.setProfileIdMSB(deviceProfileId.getId().getMostSignificantBits())
.setProfileIdLSB(deviceProfileId.getId().getLeastSignificantBits())
.build();
TbProtoQueueMsg<TransportApiRequestMsg> protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(),
TransportApiRequestMsg.newBuilder().setGetDeviceProfileRequestMsg(msg).build());
AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg),
response -> {
ByteString devProfileBody = response.getValue().getGetDeviceProfileResponseMsg().getData();
if (devProfileBody != null && !devProfileBody.isEmpty()) {
DeviceProfile profile = transportProfileCache.put(devProfileBody);
if (profile != null) {
callback.onSuccess(profile);
} else {
log.warn("Failed to decode device profile: {}", devProfileBody);
callback.onError(new IllegalArgumentException("Failed to decode device profile!"));
}
} else {
log.warn("Failed to find device profile: [{}]", deviceProfileId);
callback.onError(new IllegalArgumentException("Failed to find device profile!"));
}
}, callback::onError, transportCallbackExecutor);
}
}
@Override
public void onProfileUpdate(DeviceProfile deviceProfile) {
long deviceProfileIdMSB = deviceProfile.getId().getId().getMostSignificantBits();
@ -750,7 +742,7 @@ public class DefaultTransportService implements TransportService {
private RuleChainId resolveRuleChainId(TransportProtos.SessionInfoProto sessionInfo) {
DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB()));
DeviceProfile deviceProfile = transportProfileCache.get(deviceProfileId);
DeviceProfile deviceProfile = deviceProfileCache.get(deviceProfileId);
RuleChainId ruleChainId;
if (deviceProfile == null) {
log.warn("[{}] Device profile is null!", deviceProfileId);
@ -779,7 +771,7 @@ public class DefaultTransportService implements TransportService {
}
}
private class StatsCallback implements TbQueueCallback {
private static class StatsCallback implements TbQueueCallback {
private final TbQueueCallback callback;
private final MessagesStats stats;

155
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportTenantProfileCache.java

@ -0,0 +1,155 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.transport.service;
import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.TransportTenantProfileCache;
import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult;
import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.TenantRoutingInfo;
import org.thingsboard.server.queue.discovery.TenantRoutingInfoService;
import org.thingsboard.server.queue.util.TbTransportComponent;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Component
@TbTransportComponent
@Slf4j
public class DefaultTransportTenantProfileCache implements TransportTenantProfileCache {
private final Lock tenantProfileFetchLock = new ReentrantLock();
private final ConcurrentMap<TenantProfileId, TenantProfile> profiles = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, TenantProfileId> tenantIds = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantProfileId, Set<TenantId>> tenantProfileIds = new ConcurrentHashMap<>();
private final DataDecodingEncodingService dataDecodingEncodingService;
private TransportService transportService;
@Lazy
@Autowired
public void setTransportService(TransportService transportService) {
this.transportService = transportService;
}
public DefaultTransportTenantProfileCache(DataDecodingEncodingService dataDecodingEncodingService) {
this.dataDecodingEncodingService = dataDecodingEncodingService;
}
@Override
public TenantProfile get(TenantId tenantId) {
return getTenantProfile(tenantId);
}
@Override
public TenantProfileUpdateResult put(ByteString profileBody) {
Optional<TenantProfile> profileOpt = dataDecodingEncodingService.decode(profileBody.toByteArray());
if (profileOpt.isPresent()) {
TenantProfile newProfile = profileOpt.get();
log.trace("[{}] put: {}", newProfile.getId(), newProfile);
return new TenantProfileUpdateResult(newProfile, tenantProfileIds.get(newProfile.getId()));
} else {
log.warn("Failed to decode profile: {}", profileBody.toString());
return new TenantProfileUpdateResult(null, Collections.emptySet());
}
}
@Override
public boolean put(TenantId tenantId, TenantProfileId profileId) {
log.trace("[{}] put: {}", tenantId, profileId);
TenantProfileId oldProfileId = tenantIds.get(tenantId);
if (oldProfileId != null && !oldProfileId.equals(profileId)) {
tenantProfileIds.computeIfAbsent(oldProfileId, id -> ConcurrentHashMap.newKeySet()).remove(tenantId);
tenantIds.put(tenantId, profileId);
tenantProfileIds.computeIfAbsent(profileId, id -> ConcurrentHashMap.newKeySet()).add(tenantId);
return true;
} else {
return false;
}
}
@Override
public Set<TenantId> remove(TenantProfileId profileId) {
Set<TenantId> tenants = tenantProfileIds.remove(profileId);
if (tenants != null) {
tenants.forEach(tenantIds::remove);
}
profiles.remove(profileId);
return tenants;
}
private TenantProfile getTenantProfile(TenantId tenantId) {
TenantProfile profile = null;
TenantProfileId tenantProfileId = tenantIds.get(tenantId);
if (tenantProfileId != null) {
profile = profiles.get(tenantProfileId);
}
if (profile == null) {
tenantProfileFetchLock.lock();
try {
tenantProfileId = tenantIds.get(tenantId);
if (tenantProfileId != null) {
profile = profiles.get(tenantProfileId);
}
if (profile == null) {
TransportProtos.GetEntityProfileRequestMsg msg = TransportProtos.GetEntityProfileRequestMsg.newBuilder()
.setEntityType(EntityType.TENANT.name())
.setEntityIdMSB(tenantId.getId().getMostSignificantBits())
.setEntityIdLSB(tenantId.getId().getLeastSignificantBits())
.build();
TransportProtos.GetEntityProfileResponseMsg routingInfo = transportService.getRoutingInfo(msg);
Optional<TenantProfile> profileOpt = dataDecodingEncodingService.decode(routingInfo.getData().toByteArray());
if (profileOpt.isPresent()) {
profile = profileOpt.get();
TenantProfile existingProfile = profiles.get(profile.getId());
if (existingProfile != null) {
profile = existingProfile;
} else {
profiles.put(profile.getId(), profile);
}
tenantProfileIds.computeIfAbsent(profile.getId(), id -> ConcurrentHashMap.newKeySet()).add(tenantId);
tenantIds.put(tenantId, profile.getId());
} else {
log.warn("[{}] Can't decode tenant profile: {}", tenantId, routingInfo.getData());
throw new RuntimeException("Can't decode tenant profile!");
}
}
} finally {
tenantProfileFetchLock.unlock();
}
}
return profile;
}
}

24
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportTenantRoutingInfoService.java

@ -16,14 +16,11 @@
package org.thingsboard.server.common.transport.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg;
import org.thingsboard.server.common.transport.TransportTenantProfileCache;
import org.thingsboard.server.queue.discovery.TenantRoutingInfo;
import org.thingsboard.server.queue.discovery.TenantRoutingInfoService;
@ -32,21 +29,16 @@ import org.thingsboard.server.queue.discovery.TenantRoutingInfoService;
@ConditionalOnExpression("'${service.type:null}'=='tb-transport'")
public class TransportTenantRoutingInfoService implements TenantRoutingInfoService {
private TransportService transportService;
private TransportTenantProfileCache tenantProfileCache;
@Lazy
@Autowired
public void setTransportService(TransportService transportService) {
this.transportService = transportService;
public TransportTenantRoutingInfoService(TransportTenantProfileCache tenantProfileCache) {
this.tenantProfileCache = tenantProfileCache;
}
@Override
public TenantRoutingInfo getRoutingInfo(TenantId tenantId) {
GetTenantRoutingInfoRequestMsg msg = GetTenantRoutingInfoRequestMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.build();
GetTenantRoutingInfoResponseMsg routingInfo = transportService.getRoutingInfo(msg);
return new TenantRoutingInfo(tenantId, routingInfo.getIsolatedTbCore(), routingInfo.getIsolatedTbRuleEngine());
TenantProfile profile = tenantProfileCache.get(tenantId);
return new TenantRoutingInfo(tenantId, profile.isIsolatedTbCore(), profile.isIsolatedTbRuleEngine());
}
}

2
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/DataDecodingEncodingService.java

@ -15,8 +15,6 @@
*/
package org.thingsboard.server.common.transport.util;
import org.thingsboard.server.common.msg.TbActorMsg;
import java.util.Optional;
public interface DataDecodingEncodingService {

12
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java

@ -32,19 +32,19 @@ public class OAuth2Utils {
}
public static OAuth2ClientsParams toOAuth2Params(List<ExtendedOAuth2ClientRegistrationInfo> extendedOAuth2ClientRegistrationInfos) {
Map<OAuth2ClientRegistrationInfoId, Set<DomainInfo>> domainsByInfoId = new HashMap<>();
Map<OAuth2ClientRegistrationInfoId, OAuth2ClientRegistrationInfo> infoById = new HashMap<>();
Map<OAuth2ClientRegistrationInfoId, List<DomainInfo>> domainsByInfoId = new LinkedHashMap<>();
Map<OAuth2ClientRegistrationInfoId, OAuth2ClientRegistrationInfo> infoById = new LinkedHashMap<>();
for (ExtendedOAuth2ClientRegistrationInfo extendedClientRegistrationInfo : extendedOAuth2ClientRegistrationInfos) {
String domainName = extendedClientRegistrationInfo.getDomainName();
SchemeType domainScheme = extendedClientRegistrationInfo.getDomainScheme();
domainsByInfoId.computeIfAbsent(extendedClientRegistrationInfo.getId(), key -> new HashSet<>())
domainsByInfoId.computeIfAbsent(extendedClientRegistrationInfo.getId(), key -> new ArrayList<>())
.add(new DomainInfo(domainScheme, domainName));
infoById.put(extendedClientRegistrationInfo.getId(), extendedClientRegistrationInfo);
}
Map<Set<DomainInfo>, OAuth2ClientsDomainParams> domainParamsMap = new HashMap<>();
Map<List<DomainInfo>, OAuth2ClientsDomainParams> domainParamsMap = new LinkedHashMap<>();
domainsByInfoId.forEach((clientRegistrationInfoId, domainInfos) -> {
domainParamsMap.computeIfAbsent(domainInfos,
key -> new OAuth2ClientsDomainParams(key, new HashSet<>())
key -> new OAuth2ClientsDomainParams(key, new ArrayList<>())
)
.getClientRegistrations()
.add(toClientRegistrationDto(infoById.get(clientRegistrationInfoId)));
@ -52,7 +52,7 @@ public class OAuth2Utils {
boolean enabled = extendedOAuth2ClientRegistrationInfos.stream()
.map(OAuth2ClientRegistrationInfo::isEnabled)
.findFirst().orElse(false);
return new OAuth2ClientsParams(enabled, new HashSet<>(domainParamsMap.values()));
return new OAuth2ClientsParams(enabled, new ArrayList<>(domainParamsMap.values()));
}
public static ClientRegistrationDto toClientRegistrationDto(OAuth2ClientRegistrationInfo oAuth2ClientRegistrationInfo) {

93
dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.service;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.junit.After;
import org.junit.Assert;
@ -29,7 +30,7 @@ import java.util.*;
import java.util.stream.Collectors;
public class BaseOAuth2ServiceTest extends AbstractServiceTest {
private static final OAuth2ClientsParams EMPTY_PARAMS = new OAuth2ClientsParams(false, new HashSet<>());
private static final OAuth2ClientsParams EMPTY_PARAMS = new OAuth2ClientsParams(false, new ArrayList<>());
@Autowired
protected OAuth2Service oAuth2Service;
@ -48,14 +49,14 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
@Test(expected = DataValidationException.class)
public void testSaveHttpAndMixedDomainsTogether() {
OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet(
OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList(
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(),
DomainInfo.builder().name("first-domain").scheme(SchemeType.MIXED).build(),
DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build()
))
.clientRegistrations(Sets.newHashSet(
.clientRegistrations(Lists.newArrayList(
validClientRegistrationDto(),
validClientRegistrationDto(),
validClientRegistrationDto()
@ -67,14 +68,14 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
@Test(expected = DataValidationException.class)
public void testSaveHttpsAndMixedDomainsTogether() {
OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet(
OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList(
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTPS).build(),
DomainInfo.builder().name("first-domain").scheme(SchemeType.MIXED).build(),
DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build()
))
.clientRegistrations(Sets.newHashSet(
.clientRegistrations(Lists.newArrayList(
validClientRegistrationDto(),
validClientRegistrationDto(),
validClientRegistrationDto()
@ -131,20 +132,20 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
Assert.assertNotNull(foundClientsParams);
Assert.assertEquals(clientsParams, foundClientsParams);
OAuth2ClientsParams newClientsParams = new OAuth2ClientsParams(true, Sets.newHashSet(
OAuth2ClientsParams newClientsParams = new OAuth2ClientsParams(true, Lists.newArrayList(
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("another-domain").scheme(SchemeType.HTTPS).build()
))
.clientRegistrations(Sets.newHashSet(
.clientRegistrations(Lists.newArrayList(
validClientRegistrationDto()
))
.build(),
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("test-domain").scheme(SchemeType.MIXED).build()
))
.clientRegistrations(Sets.newHashSet(
.clientRegistrations(Lists.newArrayList(
validClientRegistrationDto()
))
.build()
@ -157,22 +158,22 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
@Test
public void testGetOAuth2Clients() {
Set<ClientRegistrationDto> firstGroup = Sets.newHashSet(
List<ClientRegistrationDto> firstGroup = Lists.newArrayList(
validClientRegistrationDto(),
validClientRegistrationDto(),
validClientRegistrationDto(),
validClientRegistrationDto()
);
Set<ClientRegistrationDto> secondGroup = Sets.newHashSet(
List<ClientRegistrationDto> secondGroup = Lists.newArrayList(
validClientRegistrationDto(),
validClientRegistrationDto()
);
Set<ClientRegistrationDto> thirdGroup = Sets.newHashSet(
List<ClientRegistrationDto> thirdGroup = Lists.newArrayList(
validClientRegistrationDto()
);
OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet(
OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList(
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(),
DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(),
DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build()
@ -180,14 +181,14 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
.clientRegistrations(firstGroup)
.build(),
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(),
DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build()
))
.clientRegistrations(secondGroup)
.build(),
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(),
DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build()
))
@ -285,15 +286,15 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
@Test
public void testGetOAuth2ClientsForHttpAndHttps() {
Set<ClientRegistrationDto> firstGroup = Sets.newHashSet(
List<ClientRegistrationDto> firstGroup = Lists.newArrayList(
validClientRegistrationDto(),
validClientRegistrationDto(),
validClientRegistrationDto(),
validClientRegistrationDto()
);
OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet(
OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList(
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(),
DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(),
DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTPS).build()
@ -335,25 +336,25 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
@Test
public void testGetDisabledOAuth2Clients() {
OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet(
OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList(
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(),
DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(),
DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build()
))
.clientRegistrations(Sets.newHashSet(
.clientRegistrations(Lists.newArrayList(
validClientRegistrationDto(),
validClientRegistrationDto(),
validClientRegistrationDto()
))
.build(),
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(),
DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build()
))
.clientRegistrations(Sets.newHashSet(
.clientRegistrations(Lists.newArrayList(
validClientRegistrationDto(),
validClientRegistrationDto()
))
@ -374,35 +375,35 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
@Test
public void testFindAllClientRegistrationInfos() {
OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet(
OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList(
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(),
DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(),
DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build()
))
.clientRegistrations(Sets.newHashSet(
.clientRegistrations(Lists.newArrayList(
validClientRegistrationDto(),
validClientRegistrationDto(),
validClientRegistrationDto()
))
.build(),
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(),
DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build()
))
.clientRegistrations(Sets.newHashSet(
.clientRegistrations(Lists.newArrayList(
validClientRegistrationDto(),
validClientRegistrationDto()
))
.build(),
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(),
DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build()
))
.clientRegistrations(Sets.newHashSet(
.clientRegistrations(Lists.newArrayList(
validClientRegistrationDto()
))
.build()
@ -423,35 +424,35 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
@Test
public void testFindClientRegistrationById() {
OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet(
OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList(
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(),
DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(),
DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build()
))
.clientRegistrations(Sets.newHashSet(
.clientRegistrations(Lists.newArrayList(
validClientRegistrationDto(),
validClientRegistrationDto(),
validClientRegistrationDto()
))
.build(),
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(),
DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build()
))
.clientRegistrations(Sets.newHashSet(
.clientRegistrations(Lists.newArrayList(
validClientRegistrationDto(),
validClientRegistrationDto()
))
.build(),
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(),
DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build()
))
.clientRegistrations(Sets.newHashSet(
.clientRegistrations(Lists.newArrayList(
validClientRegistrationDto()
))
.build()
@ -466,14 +467,14 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
}
private OAuth2ClientsParams createDefaultClientsParams() {
return new OAuth2ClientsParams(true, Sets.newHashSet(
return new OAuth2ClientsParams(true, Lists.newArrayList(
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(),
DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(),
DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build()
))
.clientRegistrations(Sets.newHashSet(
.clientRegistrations(Lists.newArrayList(
validClientRegistrationDto(),
validClientRegistrationDto(),
validClientRegistrationDto(),
@ -481,11 +482,11 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
))
.build(),
OAuth2ClientsDomainParams.builder()
.domainInfos(Sets.newHashSet(
.domainInfos(Lists.newArrayList(
DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(),
DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build()
))
.clientRegistrations(Sets.newHashSet(
.clientRegistrations(Lists.newArrayList(
validClientRegistrationDto(),
validClientRegistrationDto()
))

51
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java

@ -19,18 +19,40 @@ import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.mqtt.*;
import io.netty.handler.codec.mqtt.MqttDecoder;
import io.netty.handler.codec.mqtt.MqttEncoder;
import io.netty.handler.codec.mqtt.MqttFixedHeader;
import io.netty.handler.codec.mqtt.MqttMessage;
import io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader;
import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttPublishMessage;
import io.netty.handler.codec.mqtt.MqttPublishVariableHeader;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.netty.handler.codec.mqtt.MqttSubscribeMessage;
import io.netty.handler.codec.mqtt.MqttSubscribePayload;
import io.netty.handler.codec.mqtt.MqttTopicSubscription;
import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage;
import io.netty.handler.codec.mqtt.MqttUnsubscribePayload;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.collection.IntObjectHashMap;
import io.netty.util.concurrent.DefaultPromise;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise;
import java.util.*;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@ -41,11 +63,11 @@ import java.util.concurrent.atomic.AtomicInteger;
final class MqttClientImpl implements MqttClient {
private final Set<String> serverSubscriptions = new HashSet<>();
private final IntObjectHashMap<MqttPendingUnsubscription> pendingServerUnsubscribes = new IntObjectHashMap<>();
private final IntObjectHashMap<MqttIncomingQos2Publish> qos2PendingIncomingPublishes = new IntObjectHashMap<>();
private final IntObjectHashMap<MqttPendingPublish> pendingPublishes = new IntObjectHashMap<>();
private final ConcurrentMap<Integer, MqttPendingUnsubscription> pendingServerUnsubscribes = new ConcurrentHashMap<>();
private final ConcurrentMap<Integer, MqttIncomingQos2Publish> qos2PendingIncomingPublishes = new ConcurrentHashMap<>();
private final ConcurrentMap<Integer, MqttPendingPublish> pendingPublishes = new ConcurrentHashMap<>();
private final HashMultimap<String, MqttSubscription> subscriptions = HashMultimap.create();
private final IntObjectHashMap<MqttPendingSubscription> pendingSubscriptions = new IntObjectHashMap<>();
private final ConcurrentMap<Integer, MqttPendingSubscription> pendingSubscriptions = new ConcurrentHashMap<>();
private final Set<String> pendingSubscribeTopics = new HashSet<>();
private final HashMultimap<MqttHandler, MqttSubscription> handlerToSubscribtion = HashMultimap.create();
private final AtomicInteger nextMessageId = new AtomicInteger(1);
@ -340,6 +362,7 @@ final class MqttClientImpl implements MqttClient {
MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader(topic, getNewMessageId().messageId());
MqttPublishMessage message = new MqttPublishMessage(fixedHeader, variableHeader, payload);
MqttPendingPublish pendingPublish = new MqttPendingPublish(variableHeader.packetId(), future, payload.retain(), message, qos);
this.pendingPublishes.put(pendingPublish.getMessageId(), pendingPublish);
ChannelFuture channelFuture = this.sendAndFlushPacket(message);
if (channelFuture != null) {
@ -350,10 +373,12 @@ final class MqttClientImpl implements MqttClient {
}
}
if (pendingPublish.isSent() && pendingPublish.getQos() == MqttQoS.AT_MOST_ONCE) {
this.pendingPublishes.remove(pendingPublish.getMessageId());
pendingPublish.getFuture().setSuccess(null); //We don't get an ACK for QOS 0
} else if (pendingPublish.isSent()) {
this.pendingPublishes.put(pendingPublish.getMessageId(), pendingPublish);
pendingPublish.startPublishRetransmissionTimer(this.eventLoop.next(), this::sendAndFlushPacket);
} else {
this.pendingPublishes.remove(pendingPublish.getMessageId());
}
return future;
}
@ -465,7 +490,7 @@ final class MqttClientImpl implements MqttClient {
}
}
IntObjectHashMap<MqttPendingSubscription> getPendingSubscriptions() {
ConcurrentMap<Integer, MqttPendingSubscription> getPendingSubscriptions() {
return pendingSubscriptions;
}
@ -485,15 +510,15 @@ final class MqttClientImpl implements MqttClient {
return serverSubscriptions;
}
IntObjectHashMap<MqttPendingUnsubscription> getPendingServerUnsubscribes() {
ConcurrentMap<Integer, MqttPendingUnsubscription> getPendingServerUnsubscribes() {
return pendingServerUnsubscribes;
}
IntObjectHashMap<MqttPendingPublish> getPendingPublishes() {
ConcurrentMap<Integer, MqttPendingPublish> getPendingPublishes() {
return pendingPublishes;
}
IntObjectHashMap<MqttIncomingQos2Publish> getQos2PendingIncomingPublishes() {
ConcurrentMap<Integer, MqttIncomingQos2Publish> getQos2PendingIncomingPublishes() {
return qos2PendingIncomingPublishes;
}

392
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -38,10 +38,18 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileInfo;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantInfo;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
@ -50,6 +58,7 @@ import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetSearchQuery;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog;
@ -60,28 +69,41 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.DefaultRuleChainCreateRequest;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainData;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
@ -325,6 +347,19 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}
}
public Optional<AssetInfo> getAssetInfoById(AssetId assetId) {
try {
ResponseEntity<AssetInfo> asset = restTemplate.getForEntity(baseURL + "/api/asset/info/{assetId}", AssetInfo.class, assetId.getId());
return Optional.ofNullable(asset.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public Asset saveAsset(Asset asset) {
return restTemplate.postForEntity(baseURL + "/api/asset", asset, Asset.class).getBody();
}
@ -390,6 +425,20 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
return assets.getBody();
}
public PageData<AssetInfo> getTenantAssetInfos(PageLink pageLink, String assetType) {
Map<String, String> params = new HashMap<>();
params.put("type", assetType);
addPageLinkToParam(params, pageLink);
ResponseEntity<PageData<AssetInfo>> assets = restTemplate.exchange(
baseURL + "/api/tenant/assetInfos?type={type}&" + getUrlParams(pageLink),
HttpMethod.GET, HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<AssetInfo>>() {
},
params);
return assets.getBody();
}
public Optional<Asset> getTenantAsset(String assetName) {
try {
ResponseEntity<Asset> asset = restTemplate.getForEntity(baseURL + "/api/tenant/assets?assetName={assetName}", Asset.class, assetName);
@ -419,6 +468,22 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
return assets.getBody();
}
public PageData<AssetInfo> getCustomerAssetInfos(CustomerId customerId, PageLink pageLink, String assetType) {
Map<String, String> params = new HashMap<>();
params.put("customerId", customerId.getId().toString());
params.put("type", assetType);
addPageLinkToParam(params, pageLink);
ResponseEntity<PageData<AssetInfo>> assets = restTemplate.exchange(
baseURL + "/api/customer/{customerId}/assetInfos?type={type}&" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<AssetInfo>>() {
},
params);
return assets.getBody();
}
public List<Asset> getAssetsByIds(List<AssetId> assetIds) {
return restTemplate.exchange(
baseURL + "/api/assets?assetIds={assetIds}",
@ -593,11 +658,15 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}
public Optional<JsonNode> activateUser(UserId userId, String password) {
return activateUser(userId, password, true);
}
public Optional<JsonNode> activateUser(UserId userId, String password, boolean sendActivationMail) {
ObjectNode activateRequest = objectMapper.createObjectNode();
activateRequest.put("activateToken", getActivateToken(userId));
activateRequest.put("password", password);
try {
ResponseEntity<JsonNode> jsonNode = restTemplate.postForEntity(baseURL + "/api/noauth/activate", activateRequest, JsonNode.class);
ResponseEntity<JsonNode> jsonNode = restTemplate.postForEntity(baseURL + "/api/noauth/activate?sendActivationMail={sendActivationMail}", activateRequest, JsonNode.class, sendActivationMail);
return Optional.ofNullable(jsonNode.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
@ -936,8 +1005,25 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}
}
public Optional<DeviceInfo> getDeviceInfoById(DeviceId deviceId) {
try {
ResponseEntity<DeviceInfo> device = restTemplate.getForEntity(baseURL + "/api/device/info/{deviceId}", DeviceInfo.class, deviceId);
return Optional.ofNullable(device.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public Device saveDevice(Device device) {
return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
return saveDevice(device, null);
}
public Device saveDevice(Device device, String accessToken) {
return restTemplate.postForEntity(baseURL + "/api/device?accessToken={accessToken}", device, Device.class, accessToken).getBody();
}
public void deleteDevice(DeviceId deviceId) {
@ -1011,6 +1097,18 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}, params).getBody();
}
public PageData<DeviceInfo> getTenantDeviceInfos(String type, DeviceProfileId deviceProfileId, PageLink pageLink) {
Map<String, String> params = new HashMap<>();
params.put("type", type);
params.put("deviceProfileId", deviceProfileId != null ? deviceProfileId.toString() : null);
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/tenant/deviceInfos?type={type}&deviceProfileId={deviceProfileId}&" + getUrlParams(pageLink),
HttpMethod.GET, HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<DeviceInfo>>() {
}, params).getBody();
}
public Optional<Device> getTenantDevice(String deviceName) {
try {
ResponseEntity<Device> device = restTemplate.getForEntity(baseURL + "/api/tenant/devices?deviceName={deviceName}", Device.class, deviceName);
@ -1036,6 +1134,19 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}, params).getBody();
}
public PageData<DeviceInfo> getCustomerDeviceInfos(CustomerId customerId, String deviceType, DeviceProfileId deviceProfileId, PageLink pageLink) {
Map<String, String> params = new HashMap<>();
params.put("customerId", customerId.toString());
params.put("type", deviceType);
params.put("deviceProfileId", deviceProfileId != null ? deviceProfileId.toString() : null);
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/customer/{customerId}/devices?type={type}&deviceProfileId={deviceProfileId}&" + getUrlParams(pageLink),
HttpMethod.GET, HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<DeviceInfo>>() {
}, params).getBody();
}
public List<Device> getDevicesByIds(List<DeviceId> deviceIds) {
return restTemplate.exchange(baseURL + "/api/devices?deviceIds={deviceIds}",
HttpMethod.GET,
@ -1074,6 +1185,12 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
restTemplate.delete(baseURL + "/api/customer/device/{deviceName}/claim", deviceName);
}
public Device assignDeviceToTenant(TenantId tenantId, DeviceId deviceId) {
return restTemplate.postForEntity(
baseURL + "/api/tenant/{tenantId}/device/{deviceId}",
HttpEntity.EMPTY, Device.class, tenantId, deviceId).getBody();
}
@Deprecated
public Device createDevice(String name, String type) {
Device device = new Device();
@ -1138,6 +1255,91 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
customerId.toString(), deviceId.toString()).getBody();
}
public Optional<DeviceProfile> getDeviceProfileById(DeviceProfileId deviceProfileId) {
try {
ResponseEntity<DeviceProfile> deviceProfile = restTemplate.getForEntity(baseURL + "/api/deviceProfile/{deviceProfileId}", DeviceProfile.class, deviceProfileId);
return Optional.ofNullable(deviceProfile.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public Optional<DeviceProfileInfo> getDeviceProfileInfoById(DeviceProfileId deviceProfileId) {
try {
ResponseEntity<DeviceProfileInfo> deviceProfileInfo = restTemplate.getForEntity(baseURL + "/api/deviceProfileInfo/{deviceProfileId}", DeviceProfileInfo.class, deviceProfileId);
return Optional.ofNullable(deviceProfileInfo.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public DeviceProfileInfo getDefaultDeviceProfileInfo() {
return restTemplate.getForEntity(baseURL + "/api/deviceProfileInfo/default", DeviceProfileInfo.class).getBody();
}
public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) {
return restTemplate.postForEntity(baseURL + "/api/deviceProfile", deviceProfile, DeviceProfile.class).getBody();
}
public void deleteDeviceProfile(DeviceProfileId deviceProfileId) {
restTemplate.delete(baseURL + "/api/deviceProfile/{deviceProfileId}", deviceProfileId);
}
public DeviceProfile setDefaultDeviceProfile(DeviceProfileId deviceProfileId) {
return restTemplate.postForEntity(
baseURL + "/api/deviceProfile/{deviceProfileId}/default",
HttpEntity.EMPTY, DeviceProfile.class, deviceProfileId).getBody();
}
public PageData<DeviceProfile> getTenantDevices(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/deviceProfiles?" + getUrlParams(pageLink),
HttpMethod.GET, HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<DeviceProfile>>() {
}, params).getBody();
}
public PageData<DeviceProfileInfo> getDeviceProfileInfos(PageLink pageLink, DeviceTransportType deviceTransportType) {
Map<String, String> params = new HashMap<>();
params.put("deviceTransportType", deviceTransportType != null ? deviceTransportType.name() : null);
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/deviceProfileInfos?deviceTransportType={deviceTransportType}&" + getUrlParams(pageLink),
HttpMethod.GET, HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<DeviceProfileInfo>>() {
}, params).getBody();
}
public Long countEntitiesByQuery(EntityCountQuery query) {
return restTemplate.postForObject(baseURL + "/api/entitiesQuery/count", query, Long.class);
}
public PageData<EntityData> findEntityDataByQuery(EntityDataQuery query) {
return restTemplate.exchange(
baseURL + "/api/entitiesQuery/find",
HttpMethod.POST, new HttpEntity<>(query),
new ParameterizedTypeReference<PageData<EntityData>>() {
}).getBody();
}
public PageData<AlarmData> findAlarmDataByQuery(AlarmDataQuery query) {
return restTemplate.exchange(
baseURL + "/api/alarmsQuery/find",
HttpMethod.POST, new HttpEntity<>(query),
new ParameterizedTypeReference<PageData<AlarmData>>() {
}).getBody();
}
public void saveRelation(EntityRelation relation) {
restTemplate.postForLocation(baseURL + "/api/relation", relation);
}
@ -1313,6 +1515,19 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}
}
public Optional<EntityViewInfo> getEntityViewInfoById(EntityViewId entityViewId) {
try {
ResponseEntity<EntityViewInfo> entityView = restTemplate.getForEntity(baseURL + "/api/entityView/info/{entityViewId}", EntityViewInfo.class, entityViewId);
return Optional.ofNullable(entityView.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public EntityView saveEntityView(EntityView entityView) {
return restTemplate.postForEntity(baseURL + "/api/entityView", entityView, EntityView.class).getBody();
}
@ -1373,6 +1588,19 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}, params).getBody();
}
public PageData<EntityViewInfo> getCustomerEntityViewInfos(CustomerId customerId, String entityViewType, PageLink pageLink) {
Map<String, String> params = new HashMap<>();
params.put("customerId", customerId.toString());
params.put("type", entityViewType);
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/customer/{customerId}/entityViewInfos?type={type}&" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<EntityViewInfo>>() {
}, params).getBody();
}
public PageData<EntityView> getTenantEntityViews(String entityViewType, PageLink pageLink) {
Map<String, String> params = new HashMap<>();
params.put("type", entityViewType);
@ -1385,6 +1613,18 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}, params).getBody();
}
public PageData<EntityViewInfo> getTenantEntityViewInfos(String entityViewType, PageLink pageLink) {
Map<String, String> params = new HashMap<>();
params.put("type", entityViewType);
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/tenant/entityViewInfos?type={type}&" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<EntityViewInfo>>() {
}, params).getBody();
}
public List<EntityView> findByQuery(EntityViewSearchQuery query) {
return restTemplate.exchange(baseURL + "/api/entityViews", HttpMethod.POST, new HttpEntity<>(query), new ParameterizedTypeReference<List<EntityView>>() {
}).getBody();
@ -1437,8 +1677,41 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<Event>>() {
},
params).getBody();
}, params).getBody();
}
public OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate) {
return restTemplate.postForEntity(baseURL + "/api/oauth2/config/template", clientRegistrationTemplate, OAuth2ClientRegistrationTemplate.class).getBody();
}
public void deleteClientRegistrationTemplate(OAuth2ClientRegistrationTemplateId oAuth2ClientRegistrationTemplateId) {
restTemplate.delete(baseURL + "/api/oauth2/config/template/{clientRegistrationTemplateId}", oAuth2ClientRegistrationTemplateId);
}
public List<OAuth2ClientRegistrationTemplate> getClientRegistrationTemplates() {
return restTemplate.exchange(
baseURL + "/api/oauth2/config/template",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<OAuth2ClientRegistrationTemplate>>() {
}).getBody();
}
public List<OAuth2ClientInfo> getOAuth2Clients() {
return restTemplate.exchange(
baseURL + "/api/noauth/oauth2Clients",
HttpMethod.POST,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<OAuth2ClientInfo>>() {
}).getBody();
}
public OAuth2ClientsParams getCurrentOAuth2Params() {
return restTemplate.getForEntity(baseURL + "/api/oauth2/config", OAuth2ClientsParams.class).getBody();
}
public OAuth2ClientsParams saveOAuth2Params(OAuth2ClientsParams oauth2Params) {
return restTemplate.postForEntity(baseURL + "/api/oauth2/config", oauth2Params, OAuth2ClientsParams.class).getBody();
}
public void handleOneWayDeviceRPCRequest(DeviceId deviceId, JsonNode requestBody) {
@ -1485,6 +1758,10 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
return restTemplate.postForEntity(baseURL + "/api/ruleChain", ruleChain, RuleChain.class).getBody();
}
public RuleChain saveRuleChain(DefaultRuleChainCreateRequest request) {
return restTemplate.postForEntity(baseURL + "/api/ruleChain/device/default", request, RuleChain.class).getBody();
}
public Optional<RuleChain> setRootRuleChain(RuleChainId ruleChainId) {
try {
ResponseEntity<RuleChain> ruleChain = restTemplate.postForEntity(baseURL + "/api/ruleChain/{ruleChainId}/root", null, RuleChain.class, ruleChainId.getId());
@ -1544,6 +1821,14 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}
}
public RuleChainData exportRuleChains(int limit) {
return restTemplate.getForEntity(baseURL + "/api/ruleChains/export?limit=" + limit, RuleChainData.class).getBody();
}
public void importRuleChains(RuleChainData ruleChainData, boolean overwrite) {
restTemplate.postForLocation(baseURL + "/api/ruleChains/import?overwrite=" + overwrite, ruleChainData);
}
public List<String> getAttributeKeys(EntityId entityId) {
return restTemplate.exchange(
baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/keys/attributes",
@ -1805,6 +2090,19 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}
}
public Optional<TenantInfo> getTenantInfoById(TenantId tenantId) {
try {
ResponseEntity<TenantInfo> tenant = restTemplate.getForEntity(baseURL + "/api/tenant/info/{tenantId}", TenantInfo.class, tenantId);
return Optional.ofNullable(tenant.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public Tenant saveTenant(Tenant tenant) {
return restTemplate.postForEntity(baseURL + "/api/tenant", tenant, Tenant.class).getBody();
}
@ -1824,6 +2122,81 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}, params).getBody();
}
public PageData<TenantInfo> getTenantInfos(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/tenantInfos?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<TenantInfo>>() {
}, params).getBody();
}
public Optional<TenantProfile> getTenantProfileById(TenantProfileId tenantProfileId) {
try {
ResponseEntity<TenantProfile> tenantProfile = restTemplate.getForEntity(baseURL + "/api/tenantProfile/{tenantProfileId}", TenantProfile.class, tenantProfileId);
return Optional.ofNullable(tenantProfile.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public Optional<EntityInfo> getTenantProfileInfoById(TenantProfileId tenantProfileId) {
try {
ResponseEntity<EntityInfo> entityInfo = restTemplate.getForEntity(baseURL + "/api/tenantProfileInfo/{tenantProfileId}", EntityInfo.class, tenantProfileId);
return Optional.ofNullable(entityInfo.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public EntityInfo getDefaultTenantProfileInfo() {
return restTemplate.getForEntity(baseURL + "/api/tenantProfileInfo/default", EntityInfo.class).getBody();
}
public TenantProfile saveTenantProfile(TenantProfile tenantProfile) {
return restTemplate.postForEntity(baseURL + "/api/tenantProfile", tenantProfile, TenantProfile.class).getBody();
}
public void deleteTenantProfile(TenantProfileId tenantProfileId) {
restTemplate.delete(baseURL + "/api/tenantProfile/{tenantProfileId}", tenantProfileId);
}
public TenantProfile setDefaultTenantProfile(TenantProfileId tenantProfileId) {
return restTemplate.exchange(baseURL + "/api/tenantProfile/{tenantProfileId}/default", HttpMethod.POST, HttpEntity.EMPTY, TenantProfile.class, tenantProfileId).getBody();
}
public PageData<TenantProfile> getTenantProfiles(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/tenantProfiles" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<TenantProfile>>() {
}, params).getBody();
}
public PageData<EntityInfo> getTenantProfileInfos(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/tenantProfileInfos" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<EntityInfo>>() {
}, params).getBody();
}
public Optional<User> getUserById(UserId userId) {
try {
ResponseEntity<User> user = restTemplate.getForEntity(baseURL + "/api/user/{userId}", User.class, userId.getId());
@ -1870,6 +2243,17 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
restTemplate.delete(baseURL + "/api/user/{userId}", userId.getId());
}
public PageData<User> getUsers(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/users" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<User>>() {
}, params).getBody();
}
public PageData<User> getTenantAdmins(TenantId tenantId, PageLink pageLink) {
Map<String, String> params = new HashMap<>();
params.put("tenantId", tenantId.getId().toString());

7
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceProfileCache.java

@ -18,8 +18,11 @@ package org.thingsboard.rule.engine.api;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.function.Consumer;
/**
* Created by ashvayka on 02.04.18.
*/
@ -29,4 +32,8 @@ public interface RuleEngineDeviceProfileCache {
DeviceProfile get(TenantId tenantId, DeviceId deviceId);
void addListener(TenantId tenantId, EntityId listenerId, Consumer<DeviceProfile> listener);
void removeListener(TenantId tenantId, EntityId listenerId);
}

5
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -20,6 +20,7 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.EntityId;
@ -223,4 +224,8 @@ public interface TbContext {
RuleNodeState saveRuleNodeState(RuleNodeState state);
void clearRuleNodeStates();
void addProfileListener(Consumer<DeviceProfile> listener);
void removeProfileListener();
}

22
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2020 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.rule.engine.profile;
public enum AlarmEvalResult {
FALSE, NOT_YET_TRUE, TRUE;
}

30
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java

@ -123,17 +123,17 @@ class AlarmRuleState {
}
}
public boolean eval(DataSnapshot data) {
public AlarmEvalResult eval(DataSnapshot data) {
boolean active = isActive(data.getTs());
switch (spec.getType()) {
case SIMPLE:
return active && eval(alarmRule.getCondition(), data);
return (active && eval(alarmRule.getCondition(), data)) ? AlarmEvalResult.TRUE : AlarmEvalResult.FALSE;
case DURATION:
return evalDuration(data, active);
case REPEATING:
return evalRepeating(data, active);
default:
return false;
return AlarmEvalResult.FALSE;
}
}
@ -203,17 +203,17 @@ class AlarmRuleState {
}
}
private boolean evalRepeating(DataSnapshot data, boolean active) {
private AlarmEvalResult evalRepeating(DataSnapshot data, boolean active) {
if (active && eval(alarmRule.getCondition(), data)) {
state.setEventCount(state.getEventCount() + 1);
updateFlag = true;
return state.getEventCount() >= requiredRepeats;
return state.getEventCount() >= requiredRepeats ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE;
} else {
return false;
return AlarmEvalResult.FALSE;
}
}
private boolean evalDuration(DataSnapshot data, boolean active) {
private AlarmEvalResult evalDuration(DataSnapshot data, boolean active) {
if (active && eval(alarmRule.getCondition(), data)) {
if (state.getLastEventTs() > 0) {
if (data.getTs() > state.getLastEventTs()) {
@ -226,24 +226,28 @@ class AlarmRuleState {
state.setDuration(0L);
updateFlag = true;
}
return state.getDuration() > requiredDurationInMs;
return state.getDuration() > requiredDurationInMs ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE;
} else {
return false;
return AlarmEvalResult.FALSE;
}
}
public boolean eval(long ts) {
public AlarmEvalResult eval(long ts) {
switch (spec.getType()) {
case SIMPLE:
case REPEATING:
return false;
return AlarmEvalResult.NOT_YET_TRUE;
case DURATION:
if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && ts > state.getLastEventTs()) {
long duration = state.getDuration() + (ts - state.getLastEventTs());
return duration > requiredDurationInMs && isActive(ts);
if (isActive(ts)) {
return duration > requiredDurationInMs ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE;
} else {
return AlarmEvalResult.FALSE;
}
}
default:
return false;
return AlarmEvalResult.FALSE;
}
}

38
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java

@ -72,7 +72,7 @@ class AlarmState {
return createOrClearAlarms(ctx, ts, null, AlarmRuleState::eval);
}
public <T> boolean createOrClearAlarms(TbContext ctx, T data, SnapshotUpdate update, BiFunction<AlarmRuleState, T, Boolean> evalFunction) {
public <T> boolean createOrClearAlarms(TbContext ctx, T data, SnapshotUpdate update, BiFunction<AlarmRuleState, T, AlarmEvalResult> evalFunction) {
boolean stateUpdate = false;
AlarmSeverity resultSeverity = null;
log.debug("[{}] processing update: {}", alarmDefinition.getId(), data);
@ -81,22 +81,28 @@ class AlarmState {
log.debug("[{}][{}] Update is not valid for current rule state", alarmDefinition.getId(), state.getSeverity());
continue;
}
boolean evalResult = evalFunction.apply(state, data);
AlarmEvalResult evalResult = evalFunction.apply(state, data);
stateUpdate |= state.checkUpdate();
if (evalResult) {
if (AlarmEvalResult.TRUE.equals(evalResult)) {
resultSeverity = state.getSeverity();
break;
} else if (AlarmEvalResult.FALSE.equals(evalResult)) {
state.clear();
stateUpdate |= state.checkUpdate();
}
}
if (resultSeverity != null) {
pushMsg(ctx, calculateAlarmResult(ctx, resultSeverity));
TbAlarmResult result = calculateAlarmResult(ctx, resultSeverity);
if (result != null) {
pushMsg(ctx, result);
}
} else if (currentAlarm != null && clearState != null) {
if (!validateUpdate(update, clearState)) {
log.debug("[{}] Update is not valid for current clear state", alarmDefinition.getId());
return stateUpdate;
}
Boolean evalResult = evalFunction.apply(clearState, data);
if (evalResult) {
AlarmEvalResult evalResult = evalFunction.apply(clearState, data);
if (AlarmEvalResult.TRUE.equals(evalResult)) {
stateUpdate |= clearState.checkUpdate();
for (AlarmRuleState state : createRulesSortedBySeverityDesc) {
state.clear();
@ -105,6 +111,9 @@ class AlarmState {
ctx.getAlarmService().clearAlarm(ctx.getTenantId(), currentAlarm.getId(), JacksonUtil.OBJECT_MAPPER.createObjectNode(), System.currentTimeMillis());
pushMsg(ctx, new TbAlarmResult(false, false, true, currentAlarm));
currentAlarm = null;
} else if (AlarmEvalResult.FALSE.equals(evalResult)) {
clearState.clear();
stateUpdate |= clearState.checkUpdate();
}
}
return stateUpdate;
@ -183,13 +192,18 @@ class AlarmState {
// Maybe we should fetch alarm every time?
currentAlarm.setEndTs(System.currentTimeMillis());
AlarmSeverity oldSeverity = currentAlarm.getSeverity();
if (!oldSeverity.equals(severity)) {
currentAlarm.setSeverity(severity);
currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm);
return new TbAlarmResult(false, false, true, false, currentAlarm);
// Skip update if severity is decreased.
if (severity.ordinal() <= oldSeverity.ordinal()) {
if (!oldSeverity.equals(severity)) {
currentAlarm.setSeverity(severity);
currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm);
return new TbAlarmResult(false, false, true, false, currentAlarm);
} else {
currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm);
return new TbAlarmResult(false, true, false, false, currentAlarm);
}
} else {
currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm);
return new TbAlarmResult(false, true, false, false, currentAlarm);
return null;
}
} else {
currentAlarm = new Alarm();

64
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java

@ -39,6 +39,7 @@ import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.dao.util.mapping.JacksonUtil;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@ -57,16 +58,20 @@ import java.util.concurrent.TimeUnit;
)
public class TbDeviceProfileNode implements TbNode {
private static final String PERIODIC_MSG_TYPE = "TbDeviceProfilePeriodicMsg";
private static final String PROFILE_UPDATE_MSG_TYPE = "TbDeviceProfileUpdateMsg";
private TbDeviceProfileNodeConfiguration config;
private RuleEngineDeviceProfileCache cache;
private TbContext ctx;
private final Map<DeviceId, DeviceState> deviceStates = new ConcurrentHashMap<>();
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbDeviceProfileNodeConfiguration.class);
this.cache = ctx.getDeviceProfileCache();
this.ctx = ctx;
scheduleAlarmHarvesting(ctx);
ctx.addProfileListener(this::onProfileUpdate);
if (config.isFetchAlarmRulesStateOnStart()) {
log.info("[{}] Fetching alarm rule state", ctx.getSelfId());
int fetchCount = 0;
@ -95,15 +100,14 @@ public class TbDeviceProfileNode implements TbNode {
}
}
/**
* TODO: Dynamic values evaluation;
*/
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException {
EntityType originatorType = msg.getOriginator().getEntityType();
if (msg.getType().equals(PERIODIC_MSG_TYPE)) {
scheduleAlarmHarvesting(ctx);
harvestAlarms(ctx, System.currentTimeMillis());
} else if (msg.getType().equals(PROFILE_UPDATE_MSG_TYPE)) {
updateProfile(ctx, new DeviceProfileId(UUID.fromString(msg.getData())));
} else {
if (EntityType.DEVICE.equals(originatorType)) {
DeviceId deviceId = new DeviceId(msg.getOriginator().getId());
@ -119,36 +123,12 @@ public class TbDeviceProfileNode implements TbNode {
ctx.tellFailure(msg, new IllegalStateException("Device profile for device [" + deviceId + "] not found!"));
}
}
} else if (EntityType.DEVICE_PROFILE.equals(originatorType)) {
log.info("[{}] Received device profile update notification: {}", ctx.getSelfId(), msg.getData());
if (msg.getType().equals("ENTITY_UPDATED")) {
DeviceProfile deviceProfile = JacksonUtil.fromString(msg.getData(), DeviceProfile.class);
if (deviceProfile != null) {
for (DeviceState state : deviceStates.values()) {
if (deviceProfile.getId().equals(state.getProfileId())) {
state.updateProfile(ctx, deviceProfile);
}
}
}
}
ctx.tellSuccess(msg);
} else {
ctx.tellSuccess(msg);
}
}
}
public void invalidateDeviceProfileCache(DeviceId deviceId, String deviceJson) {
DeviceState deviceState = deviceStates.get(deviceId);
if (deviceState != null) {
DeviceProfileId currentProfileId = deviceState.getProfileId();
Device device = JacksonUtil.fromString(deviceJson, Device.class);
if (!currentProfileId.equals(device.getDeviceProfileId())) {
deviceStates.remove(deviceId);
}
}
}
@Override
public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) {
// Cleanup the cache for all entities that are no longer assigned to current server partitions
@ -157,6 +137,7 @@ public class TbDeviceProfileNode implements TbNode {
@Override
public void destroy() {
ctx.removeProfileListener();
deviceStates.clear();
}
@ -183,4 +164,33 @@ public class TbDeviceProfileNode implements TbNode {
}
}
protected void updateProfile(TbContext ctx, DeviceProfileId deviceProfileId) throws ExecutionException, InterruptedException {
DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceProfileId);
if (deviceProfile != null) {
log.debug("[{}] Received device profile update notification: {}", ctx.getSelfId(), deviceProfile);
for (DeviceState state : deviceStates.values()) {
if (deviceProfile.getId().equals(state.getProfileId())) {
state.updateProfile(ctx, deviceProfile);
}
}
} else {
log.debug("[{}] Received stale profile update notification: [{}]", ctx.getSelfId(), deviceProfileId);
}
}
protected void onProfileUpdate(DeviceProfile profile) {
ctx.tellSelf(TbMsg.newMsg(PROFILE_UPDATE_MSG_TYPE, ctx.getTenantId(), TbMsgMetaData.EMPTY, profile.getId().getId().toString()), 0L);
}
protected void invalidateDeviceProfileCache(DeviceId deviceId, String deviceJson) {
DeviceState deviceState = deviceStates.get(deviceId);
if (deviceState != null) {
DeviceProfileId currentProfileId = deviceState.getProfileId();
Device device = JacksonUtil.fromString(deviceJson, Device.class);
if (!currentProfileId.equals(device.getDeviceProfileId())) {
deviceStates.remove(deviceId);
}
}
}
}

4
transport/coap/src/main/resources/tb-coap-transport.yml

@ -49,10 +49,6 @@ transport:
sessions:
inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}"
report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}"
rate_limits:
enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}"
tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}"
device: "${TB_TRANSPORT_RATE_LIMITS_DEVICE:10:1,300:60}"
json:
# Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON
type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}"

4
transport/http/src/main/resources/tb-http-transport.yml

@ -42,10 +42,6 @@ transport:
sessions:
inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}"
report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}"
rate_limits:
enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}"
tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}"
device: "${TB_TRANSPORT_RATE_LIMITS_DEVICE:10:1,300:60}"
json:
# Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON
type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}"

4
transport/mqtt/src/main/resources/tb-mqtt-transport.yml

@ -71,10 +71,6 @@ transport:
sessions:
inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}"
report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}"
rate_limits:
enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}"
tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}"
device: "${TB_TRANSPORT_RATE_LIMITS_DEVICE:10:1,300:60}"
json:
# Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON
type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}"

4
ui-ngx/src/app/core/http/oauth2.service.ts

@ -41,4 +41,8 @@ export class OAuth2Service {
return this.http.post<OAuth2ClientsParams>('/api/oauth2/config', OAuth2Setting,
defaultHttpOptionsFromConfig(config));
}
public getLoginProcessingUrl(config?: RequestConfig): Observable<string> {
return this.http.get<string>(`/api/oauth2/loginProcessingUrl`, defaultHttpOptionsFromConfig(config));
}
}

5
ui-ngx/src/app/core/services/menu.service.ts

@ -191,6 +191,11 @@ export class MenuService {
name: 'admin.security-settings',
icon: 'security',
path: '/settings/security-settings'
},
{
name: 'admin.oauth2.oauth2',
icon: 'security',
path: '/settings/oauth2'
}
]
}

17
ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html

@ -88,14 +88,20 @@
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<div fxLayout="row" *ngIf="alarm$ | async; let alarm;">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="close()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
<span fxFlex></span>
<div fxLayout="row" *ngIf="alarm$ | async; let alarm;" fxLayoutGap="8px">
<button *ngIf="allowAcknowledgment && (alarm.status === alarmStatuses.ACTIVE_UNACK ||
alarm.status === alarmStatuses.CLEARED_UNACK)"
mat-raised-button
color="primary"
type="button"
(click)="acknowledge()"
style="margin-right: 20px;"
[disabled]="(isLoading$ | async)">
{{ 'alarm.acknowledge' | translate }}
</button>
@ -109,12 +115,5 @@
{{ 'alarm.clear' | translate }}
</button>
</div>
<span fxFlex></span>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="close()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
</form>

10
ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html

@ -59,16 +59,16 @@
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || entityAliasFormGroup.invalid || !entityAliasFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.save') | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || entityAliasFormGroup.invalid || !entityAliasFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.save') | translate }}
</button>
</div>
</form>

10
ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html

@ -95,11 +95,6 @@
{{ 'alias.add' | translate }}
</button>
<span fxFlex></span>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || entityAliasesFormGroup.invalid || !entityAliasesFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
@ -107,5 +102,10 @@
cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || entityAliasesFormGroup.invalid || !entityAliasesFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

10
ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html

@ -44,16 +44,16 @@
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || attributeFormGroup.invalid || !attributeFormGroup.dirty">
{{ 'action.add' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || attributeFormGroup.invalid || !attributeFormGroup.dirty">
{{ 'action.add' | translate }}
</button>
</div>
</form>

15
ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html

@ -55,21 +55,22 @@
</mat-radio-group>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<div mat-dialog-actions fxLayout="row">
<mat-checkbox formControlName="openDashboard"
style="margin-bottom: 0; padding-right: 20px;">
style="margin-bottom: 0;">
{{ 'dashboard.open-dashboard' | translate }}
</mat-checkbox>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || addWidgetFormGroup.invalid || !addWidgetFormGroup.dirty">
{{ 'action.add' | translate }}
</button>
<span fxFlex></span>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || addWidgetFormGroup.invalid || !addWidgetFormGroup.dirty">
{{ 'action.add' | translate }}
</button>
</div>
</form>

1
ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html

@ -26,7 +26,6 @@
<div fxLayout="row" class="tb-panel-actions">
<span fxFlex></span>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>

10
ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.html

@ -40,11 +40,6 @@
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || stateFormGroup.invalid">
{{ 'action.select' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
@ -52,5 +47,10 @@
cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || stateFormGroup.invalid">
{{ 'action.select' | translate }}
</button>
</div>
</form>

10
ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html

@ -33,11 +33,6 @@
<tb-anchor #entityDetailsForm></tb-anchor>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || detailsForm?.invalid || !detailsForm?.dirty">
{{ 'action.add' | translate }}
</button>
<button mat-button color="primary"
type="button"
cdkFocusInitial
@ -45,5 +40,10 @@
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || detailsForm?.invalid || !detailsForm?.dirty">
{{ 'action.add' | translate }}
</button>
</div>
</form>

1
ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html

@ -31,7 +31,6 @@
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
[mat-dialog-close]="false" cdkFocusInitial>

12
ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html

@ -46,12 +46,6 @@
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
*ngIf="!data.readonly"
type="submit"
[disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
@ -59,5 +53,11 @@
cdkFocusInitial>
{{ (data.readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button mat-raised-button color="primary"
*ngIf="!data.readonly"
type="submit"
[disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button>
</div>
</form>

10
ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html

@ -55,16 +55,16 @@
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || filterFormGroup.invalid || !filterFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || filterFormGroup.invalid || !filterFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button>
</div>
</form>

2
ui-ngx/src/app/modules/home/components/filter/filter-text.component.html

@ -15,5 +15,5 @@
limitations under the License.
-->
<div class="tb-filter-text" [ngClass]="{disabled: disabled, required: requiredClass}"
<div class="tb-filter-text" [ngClass]="{disabled: disabled, required: requiredClass, nowrap: nowrap}"
[innerHTML]="filterText"></div>

5
ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss

@ -21,6 +21,11 @@
}
&.required {
color: #f44336;
padding: 0 4px;
}
&.nowrap {
white-space: nowrap;
overflow: hidden;
}
}
}

3
ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts

@ -54,6 +54,9 @@ export class FilterTextComponent implements ControlValueAccessor, OnInit {
@Input()
addFilterPrompt = this.translate.instant('filter.add-filter-prompt');
@Input()
nowrap = false;
requiredClass = false;
private filterText: string;

12
ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html

@ -46,12 +46,6 @@
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
*ngIf="!data.readonly"
type="submit"
[disabled]="(isLoading$ | async) || filterUserInfoFormGroup.invalid || !filterUserInfoFormGroup.dirty">
{{ 'action.update' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
@ -59,5 +53,11 @@
cdkFocusInitial>
{{ (data.readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button mat-raised-button color="primary"
*ngIf="!data.readonly"
type="submit"
[disabled]="(isLoading$ | async) || filterUserInfoFormGroup.invalid || !filterUserInfoFormGroup.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>

10
ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html

@ -88,11 +88,6 @@
{{ 'filter.add' | translate }}
</button>
<span fxFlex></span>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || filtersFormGroup.invalid || !filtersFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
@ -100,5 +95,10 @@
cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || filtersFormGroup.invalid || !filtersFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

12
ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html

@ -79,12 +79,6 @@
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
*ngIf="!data.readonly"
[disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid || !keyFilterFormGroup.dirty">
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
@ -92,5 +86,11 @@
cdkFocusInitial>
{{ (data.readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
*ngIf="!data.readonly"
[disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid || !keyFilterFormGroup.dirty">
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button>
</div>
</form>

10
ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html

@ -65,16 +65,16 @@
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || userFilterFormGroup.invalid || !userFilterFormGroup.dirty">
{{ 'action.update' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || userFilterFormGroup.invalid || !userFilterFormGroup.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>

10
ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html

@ -43,16 +43,16 @@
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || importFormGroup.invalid || !importFormGroup.dirty">
{{ 'action.import' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || importFormGroup.invalid || !importFormGroup.dirty">
{{ 'action.import' | translate }}
</button>
</div>
</form>

45
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div style="min-width: 1000px;">
<div>
<mat-toolbar color="primary">
<h2 translate>device-profile.add</h2>
<span fxFlex></span>
@ -106,28 +106,25 @@
</mat-step>
</mat-horizontal-stepper>
</div>
<div mat-dialog-actions fxLayout="column" fxLayoutAlign="start wrap" fxLayoutGap="8px" style="height: 100px;">
<div fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-raised-button
*ngIf="showNext"
[disabled]="(isLoading$ | async)"
(click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
</div>
<div fxFlex fxLayout="row">
<button mat-button
color="primary"
[disabled]="(isLoading$ | async)"
(click)="cancel()">{{ 'action.cancel' | translate }}</button>
<span fxFlex></span>
<div fxLayout="row wrap" fxLayoutGap="8px">
<button mat-raised-button *ngIf="selectedIndex > 0"
[disabled]="(isLoading$ | async)"
(click)="previousStep()">{{ 'action.back' | translate }}</button>
<button mat-raised-button
[disabled]="(isLoading$ | async)"
color="primary"
(click)="add()">{{ 'action.add' | translate }}</button>
</div>
</div>
<div mat-dialog-actions fxLayout="row">
<button mat-stroked-button *ngIf="selectedIndex > 0"
[disabled]="(isLoading$ | async)"
(click)="previousStep()">{{ 'action.back' | translate }}</button>
<span fxFlex></span>
<button mat-stroked-button
color="primary"
*ngIf="showNext"
[disabled]="(isLoading$ | async)"
(click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
</div>
<mat-divider></mat-divider>
<div mat-dialog-actions fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end">
<button mat-button
[disabled]="(isLoading$ | async)"
(click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-raised-button
[disabled]="(isLoading$ | async)"
color="primary"
(click)="add()">{{ 'action.add' | translate }}</button>
</div>
</div>

4
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss

@ -28,7 +28,7 @@
display: flex;
flex-direction: column;
height: 100%;
padding: 24px 24px 8px !important;
padding: 0 !important;
.mat-stepper-horizontal {
display: flex;
@ -45,7 +45,7 @@
}
}
.mat-horizontal-content-container {
height: 350px;
height: 530px;
max-height: 100%;
width: 100%;;
overflow-y: auto;

12
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html

@ -108,17 +108,17 @@
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="(isLoading$ | async) || conditionFormGroup.invalid || !conditionFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="(isLoading$ | async) || conditionFormGroup.invalid || !conditionFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

25
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html

@ -15,25 +15,22 @@
limitations under the License.
-->
<div fxLayout="column" fxFlex [formGroup]="alarmRuleConditionFormGroup">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<label class="tb-title" translate>device-profile.condition</label>
<span fxFlex></span>
<a mat-button color="primary"
type="button"
(click)="openFilterDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
{{ (disabled ? 'action.view' : (conditionSet() ? 'action.edit' : 'action.add')) | translate }}
</a>
</div>
<div fxLayout="row" fxLayoutAlign="start center" [formGroup]="alarmRuleConditionFormGroup" style="min-width: 0;">
<div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)">
<tb-filter-text formControlName="condition"
[nowrap]="true"
required
addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
</tb-filter-text>
<span class="tb-alarm-rule-condition-spec" [ngClass]="{disabled: this.disabled}" [innerHTML]="specText">
<span *ngIf="specText" class="tb-alarm-rule-condition-spec" [ngClass]="{disabled: this.disabled}" [innerHTML]="specText">
</span>
</div>
<button mat-icon-button
[color]="conditionSet() ? 'primary' : 'warn'"
type="button"
(click)="openFilterDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
<mat-icon>{{ disabled ? 'visibility' : (conditionSet() ? 'edit' : 'add') }}</mat-icon>
</button>
</div>

7
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss

@ -22,13 +22,10 @@
}
.tb-alarm-rule-condition {
cursor: pointer;
min-width: 0;
.tb-alarm-rule-condition-spec {
margin-top: 1em;
line-height: 1.8em;
opacity: 0.7;
padding: 4px;
&.disabled {
opacity: 0.7;
}
}
}
}

45
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html

@ -16,36 +16,23 @@
-->
<div fxLayout="column" [formGroup]="alarmRuleFormGroup">
<tb-alarm-rule-condition fxFlex class="row"
formControlName="condition">
<tb-alarm-rule-condition formControlName="condition">
</tb-alarm-rule-condition>
<mat-divider class="row"></mat-divider>
<tb-alarm-schedule-info fxFlex class="row"
formControlName="schedule">
<tb-alarm-schedule-info formControlName="schedule">
</tb-alarm-schedule-info>
<mat-divider class="row"></mat-divider>
<div fxLayout="column" fxFlex class="tb-alarm-rule-details row">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<label class="tb-title" translate>device-profile.alarm-rule-details</label>
<span fxFlex></span>
<a mat-button color="primary"
*ngIf="!disabled"
type="button"
(click)="openEditDetailsDialog($event)"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above">
{{ 'action.edit' | translate }}
</a>
</div>
<div fxLayout="row" fxLayoutAlign="start start">
<div class="tb-alarm-rule-details-content" [ngClass]="{disabled: this.disabled, collapsed: !this.expandAlarmDetails}"
(click)="!disabled ? openEditDetailsDialog($event) : {}"
fxFlex [innerHTML]="alarmRuleFormGroup.get('alarmDetails').value"></div>
<a mat-button color="primary"
type="button"
(click)="expandAlarmDetails = !expandAlarmDetails">
{{ (expandAlarmDetails ? 'action.hide' : 'action.read-more') | translate }}
</a>
</div>
<div *ngIf="!disabled || alarmRuleFormGroup.get('alarmDetails').value" fxLayout="row" fxLayoutAlign="start center">
<span class="tb-alarm-rule-details title" (click)="openEditDetailsDialog($event)">
{{ alarmRuleFormGroup.get('alarmDetails').value ? ('device-profile.alarm-rule-details' | translate) + ': ' : ('device-profile.add-alarm-rule-details' | translate) }}
</span>
<span *ngIf="alarmRuleFormGroup.get('alarmDetails').value" class="tb-alarm-rule-details"
(click)="openEditDetailsDialog($event)"
[innerHTML]="alarmRuleFormGroup.get('alarmDetails').value"></span>
<button mat-icon-button color="primary"
type="button"
(click)="openEditDetailsDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
<mat-icon>{{ disabled ? 'visibility' : (alarmRuleFormGroup.get('alarmDetails').value ? 'edit' : 'add') }}</mat-icon>
</button>
</div>
</div>

30
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss

@ -14,31 +14,19 @@
* limitations under the License.
*/
:host {
min-width: 0;
.row {
margin-top: 1em;
}
.tb-alarm-rule-details {
a.mat-button {
&:hover, &:focus {
border-bottom: none;
}
}
.tb-alarm-rule-details-content {
min-height: 33px;
overflow: hidden;
white-space: pre;
line-height: 1.8em;
padding: 4px;
cursor: pointer;
&.collapsed {
max-height: 33px;
white-space: nowrap;
text-overflow: ellipsis;
}
&.disabled {
opacity: 0.7;
cursor: auto;
}
padding: 4px;
cursor: pointer;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&.title {
opacity: 0.7;
overflow: visible;
}
}
}

3
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts

@ -118,7 +118,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value
alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value,
readonly: this.disabled
}
}).afterClosed().subscribe((alarmDetails) => {
if (isDefinedAndNotNull(alarmDetails)) {

12
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html

@ -37,17 +37,17 @@
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="(isLoading$ | async) || alarmScheduleFormGroup.invalid || !alarmScheduleFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="(isLoading$ | async) || alarmScheduleFormGroup.invalid || !alarmScheduleFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

25
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.html

@ -15,19 +15,16 @@
limitations under the License.
-->
<div fxLayout="column" fxFlex>
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<label class="tb-title" translate>device-profile.schedule</label>
<span fxFlex></span>
<a mat-button color="primary"
type="button"
(click)="openScheduleDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
{{ (disabled ? 'action.view' : 'action.edit' ) | translate }}
</a>
</div>
<sapn class="tb-alarm-rule-schedule" [ngClass]="{disabled: this.disabled}" (click)="openScheduleDialog($event)"
<div fxLayout="row" fxLayoutAlign="start center" style="min-width: 0;">
<span class="tb-alarm-rule-schedule title" (click)="openScheduleDialog($event)">{{('device-profile.schedule' | translate) + ': '}}</span>
<span class="tb-alarm-rule-schedule" (click)="openScheduleDialog($event)"
[innerHTML]="scheduleText">
</sapn>
</span>
<button mat-icon-button color="primary"
type="button"
(click)="openScheduleDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
<mat-icon>{{ disabled ? 'visibility' : 'edit' }}</mat-icon>
</button>
</div>

10
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.scss

@ -21,14 +21,14 @@
}
}
.tb-alarm-rule-schedule {
line-height: 1.8em;
padding: 4px;
cursor: pointer;
&.disabled {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&.title {
opacity: 0.7;
}
.nowrap {
white-space: nowrap;
overflow: visible;
}
}
}

2
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts

@ -101,7 +101,7 @@ export class AlarmScheduleInfoComponent implements ControlValueAccessor, OnInit
for (const item of schedule.items) {
if (item.enabled) {
if (this.scheduleText.length) {
this.scheduleText += '<br/>';
this.scheduleText += ', ';
}
this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]);
this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn),

8
ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html

@ -19,7 +19,7 @@
<div *ngFor="let createAlarmRuleControl of createAlarmRulesFormArray().controls; let $index = index;
last as isLast;" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px" style="padding-bottom: 8px;" [formGroup]="createAlarmRuleControl">
<div class="create-alarm-rule" fxFlex fxLayout="column" fxLayoutGap="8px" fxLayoutAlign="start">
<div class="create-alarm-rule" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
<mat-form-field class="severity mat-block" floatLabel="always" hideRequiredMarker>
<mat-label translate>alarm.severity</mat-label>
<mat-select formControlName="severity"
@ -34,7 +34,7 @@
{{ 'device-profile.alarm-severity-required' | translate }}
</mat-error>
</mat-form-field>
<mat-divider></mat-divider>
<mat-divider vertical></mat-divider>
<tb-alarm-rule formControlName="alarmRule" required fxFlex>
</tb-alarm-rule>
</div>
@ -47,9 +47,9 @@
<mat-icon>remove_circle_outline</mat-icon>
</button>
</div>
<div *ngIf="!createAlarmRulesFormArray().controls.length">
<div *ngIf="!createAlarmRulesFormArray().controls.length && !disabled">
<span translate fxLayoutAlign="center center" style="margin: 16px 0"
class="tb-prompt">device-profile.no-create-alarm-rules</span>
class="tb-prompt required">device-profile.add-create-alarm-rule-prompt</span>
</div>
<div fxLayout="row" *ngIf="!disabled">
<button mat-stroked-button color="primary"

21
ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts

@ -23,11 +23,11 @@ import {
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
NG_VALUE_ACCESSOR, ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { AlarmRule } from '@shared/models/device.models';
import { AlarmRule, alarmRuleValidator } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog';
import { Subscription } from 'rxjs';
import { AlarmSeverity, alarmSeverityTranslations } from '../../../../../shared/models/alarm.models';
@ -141,10 +141,23 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
};
const createAlarmRulesArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray;
createAlarmRulesArray.push(this.fb.group({
severity: [null, Validators.required],
alarmRule: [createAlarmRule, Validators.required]
severity: [this.getFirstUnusedSeverity(), Validators.required],
alarmRule: [createAlarmRule, alarmRuleValidator]
}));
this.createAlarmRulesFormGroup.updateValueAndValidity();
if (!this.createAlarmRulesFormGroup.valid) {
this.updateModel();
}
}
private getFirstUnusedSeverity(): AlarmSeverity {
for (const severityKey of Object.keys(AlarmSeverity)) {
const severity = AlarmSeverity[severityKey];
if (this.usedSeverities.indexOf(severity) === -1) {
return severity;
}
}
return null;
}
public validate(c: FormControl) {

2
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html

@ -98,7 +98,7 @@
<mat-icon>remove_circle_outline</mat-icon>
</button>
</div>
<div *ngIf="!alarmFormGroup.get('clearRule').value">
<div *ngIf="disabled && !alarmFormGroup.get('clearRule').value">
<span translate fxLayoutAlign="center center" style="margin: 16px 0"
class="tb-prompt">device-profile.no-clear-alarm-rule</span>
</div>

4
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts

@ -25,7 +25,7 @@ import {
Validator,
Validators
} from '@angular/forms';
import { AlarmRule, DeviceProfileAlarm } from '@shared/models/device.models';
import { AlarmRule, DeviceProfileAlarm, deviceProfileAlarmValidator } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips';
@ -92,7 +92,7 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
clearRule: [null],
propagate: [null],
propagateRelationTypes: [null]
});
}, { validators: deviceProfileAlarmValidator });
this.alarmFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});

9
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts

@ -30,7 +30,7 @@ import {
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DeviceProfileAlarm } from '@shared/models/device.models';
import { DeviceProfileAlarm, deviceProfileAlarmValidator } from '@shared/models/device.models';
import { guid } from '@core/utils';
import { Subscription } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
@ -141,7 +141,7 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni
id: guid(),
alarmType: '',
createRules: {
empty: {
CRITICAL: {
condition: {
condition: []
}
@ -149,8 +149,11 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni
}
};
const alarmsArray = this.deviceProfileAlarmsFormGroup.get('alarms') as FormArray;
alarmsArray.push(this.fb.control(alarm, [Validators.required]));
alarmsArray.push(this.fb.control(alarm, [deviceProfileAlarmValidator]));
this.deviceProfileAlarmsFormGroup.updateValueAndValidity();
if (!this.deviceProfileAlarmsFormGroup.valid) {
this.updateModel();
}
}
public validate(c: FormControl) {

10
ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html

@ -38,16 +38,16 @@
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button *ngIf="!data.readonly" mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

4
ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts

@ -27,6 +27,7 @@ import { TranslateService } from '@ngx-translate/core';
export interface EditAlarmDetailsDialogData {
alarmDetails: string;
readonly: boolean;
}
@Component({
@ -57,6 +58,9 @@ export class EditAlarmDetailsDialogComponent extends DialogComponent<EditAlarmDe
this.editDetailsFormGroup = this.fb.group({
alarmDetails: [this.alarmDetails]
});
if (this.data.readonly) {
this.editDetailsFormGroup.disable();
}
}
ngOnInit(): void {

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save