Browse Source

Add solution template install support to IoT Hub integration

- Add SolutionService with install/delete/rollback flow, entity provisioning
  (customers, assets, devices, dashboards, edges, rule chains, CFs, users),
  telemetry emulators, instructions templating, and public customer support
- Add SolutionInstallDialogComponent in home/components/solution with markdown
  details display and "Go to main dashboard" navigation
- Update IoT Hub install dialog to open solution dialog on template success
- Add SOLUTION_TEMPLATE type support across frontend (item cards, detail dialog,
  installed items table with dashboard entity mapping)
- Add solution instructions button to item detail dialog for installed templates
- Update edge detail URLs and dashboard links for correct CE routing
pull/15347/head
Igor Kulikov 6 months ago
parent
commit
ce5625a332
  1. 11
      application/src/main/java/org/thingsboard/server/controller/IotHubController.java
  2. 65
      application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java
  3. 5
      application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java
  4. 1462
      application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java
  5. 32
      application/src/main/java/org/thingsboard/server/service/solutions/SolutionService.java
  6. 39
      application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedAlarmRuleInfo.java
  7. 39
      application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedCalculatedFieldInfo.java
  8. 49
      application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedEntityInfo.java
  9. 28
      application/src/main/java/org/thingsboard/server/service/solutions/data/DashboardLinkInfo.java
  10. 30
      application/src/main/java/org/thingsboard/server/service/solutions/data/DeviceCredentialsInfo.java
  11. 28
      application/src/main/java/org/thingsboard/server/service/solutions/data/EdgeLinkInfo.java
  12. 208
      application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java
  13. 28
      application/src/main/java/org/thingsboard/server/service/solutions/data/UserCredentialsInfo.java
  14. 39
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/AssetDefinition.java
  15. 28
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/AssetProfileDefinition.java
  16. 48
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/BaseEntityDefinition.java
  17. 28
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CalculatedFieldDefinition.java
  18. 55
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CustomerDefinition.java
  19. 28
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CustomerEntityDefinition.java
  20. 38
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DashboardDefinition.java
  21. 26
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DashboardUserDetailsDefinition.java
  22. 41
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DeviceDefinition.java
  23. 28
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DeviceProfileDefinition.java
  24. 45
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EdgeDefinition.java
  25. 64
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EmulatorDefinition.java
  26. 26
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EntityDefinition.java
  27. 31
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EntitySearchKey.java
  28. 32
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/ReferenceableEntityDefinition.java
  29. 34
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/RelationDefinition.java
  30. 29
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/TelemetryProfile.java
  31. 29
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/TenantDefinition.java
  32. 40
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/UserDefinition.java
  33. 195
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/AbstractEmulatorLauncher.java
  34. 42
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/AssetEmulatorLauncher.java
  35. 42
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/BasicEmulator.java
  36. 25
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/CustomEmulator.java
  37. 98
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/DeviceEmulatorLauncher.java
  38. 24
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/Emulator.java
  39. 108
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/FieldIrrigationStateEmulator.java
  40. 24
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/SimpleEmulator.java
  41. 26
      application/src/main/java/org/thingsboard/server/service/solutions/data/names/RandomNameData.java
  42. 100
      application/src/main/java/org/thingsboard/server/service/solutions/data/names/RandomNameUtil.java
  43. 42
      application/src/main/java/org/thingsboard/server/service/solutions/data/solution/SolutionInstallResponse.java
  44. 46
      application/src/main/java/org/thingsboard/server/service/solutions/data/solution/TenantSolutionTemplateInstructions.java
  45. 34
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/CompositeValueStrategyDefinition.java
  46. 55
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/CompositeValueStrategyGenerator.java
  47. 51
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/ConstantTelemetryGenerator.java
  48. 31
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/ConstantValueStrategyDefinition.java
  49. 69
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/CounterTelemetryGenerator.java
  50. 42
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/CounterValueStrategyDefinition.java
  51. 37
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/DecrementTelemetryGenerator.java
  52. 34
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/DecrementValueStrategyDefinition.java
  53. 54
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/EventTelemetryGenerator.java
  54. 33
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/EventValueStrategyDefinition.java
  55. 96
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/GeneratorTools.java
  56. 64
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncDecTelemetryGenerator.java
  57. 32
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncDecValueStrategyDefinition.java
  58. 36
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncrementTelemetryGenerator.java
  59. 34
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncrementValueStrategyDefinition.java
  60. 94
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/NaturalTelemetryGenerator.java
  61. 45
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/NaturalValueStrategyDefinition.java
  62. 35
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/ScheduleValueStrategyDefinition.java
  63. 70
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/ScheduleValueStrategyGenerator.java
  64. 34
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/SequenceValueStrategyDefinition.java
  65. 48
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/SequenceValueStrategyGenerator.java
  66. 41
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/TelemetryGenerator.java
  67. 48
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/TelemetryGeneratorFactory.java
  68. 39
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategyDefinition.java
  69. 22
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategyDefinitionType.java
  70. 31
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategySchedule.java
  71. 3
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java
  72. 3
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java
  73. 34
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/SolutionTemplateInstalledItemDescriptor.java
  74. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  75. 44
      ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.html
  76. 50
      ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.scss
  77. 59
      ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.ts
  78. 5
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html
  79. 4
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts
  80. 22
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts
  81. 7
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss
  82. 3
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts
  83. 2
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts
  84. 25
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html
  85. 18
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.scss
  86. 19
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.ts
  87. 12
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts
  88. 26
      ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts
  89. 10
      ui-ngx/src/assets/locale/locale.constant-en_US.json

11
application/src/main/java/org/thingsboard/server/controller/IotHubController.java

@ -29,6 +29,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
@ -58,8 +59,9 @@ public class IotHubController extends BaseController {
@PostMapping("/versions/{versionId}/install") @PostMapping("/versions/{versionId}/install")
@ResponseBody @ResponseBody
public InstallItemVersionResult installItemVersion(@PathVariable String versionId, public InstallItemVersionResult installItemVersion(@PathVariable String versionId,
@RequestBody(required = false) JsonNode data) throws ThingsboardException { @RequestBody(required = false) JsonNode data,
return iotHubService.installItemVersion(getCurrentUser(), versionId, data); HttpServletRequest request) throws ThingsboardException {
return iotHubService.installItemVersion(getCurrentUser(), versionId, data, request);
} }
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@ -67,8 +69,9 @@ public class IotHubController extends BaseController {
@ResponseBody @ResponseBody
public UpdateItemVersionResult updateItemVersion(@PathVariable UUID installedItemId, public UpdateItemVersionResult updateItemVersion(@PathVariable UUID installedItemId,
@PathVariable String versionId, @PathVariable String versionId,
@RequestParam(required = false, defaultValue = "false") boolean force) throws ThingsboardException { @RequestParam(required = false, defaultValue = "false") boolean force,
return iotHubService.updateItemVersion(getCurrentUser(), new IotHubInstalledItemId(installedItemId), versionId, force); HttpServletRequest request) throws ThingsboardException {
return iotHubService.updateItemVersion(getCurrentUser(), new IotHubInstalledItemId(installedItemId), versionId, force, request);
} }
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")

65
application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.iot_hub; package org.thingsboard.server.service.iot_hub;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -32,7 +33,10 @@ import org.thingsboard.server.common.data.iot_hub.DeviceInstalledItemDescriptor;
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem;
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor; import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor;
import org.thingsboard.server.common.data.iot_hub.RuleChainInstalledItemDescriptor; import org.thingsboard.server.common.data.iot_hub.RuleChainInstalledItemDescriptor;
import org.thingsboard.server.common.data.iot_hub.SolutionTemplateInstalledItemDescriptor;
import org.thingsboard.server.common.data.iot_hub.WidgetInstalledItemDescriptor; import org.thingsboard.server.common.data.iot_hub.WidgetInstalledItemDescriptor;
import org.thingsboard.server.service.solutions.SolutionService;
import org.thingsboard.server.service.solutions.data.solution.SolutionInstallResponse;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.NodeConnectionInfo;
import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainMetaData;
@ -76,9 +80,10 @@ public class DefaultIotHubService implements IotHubService {
private final DashboardService dashboardService; private final DashboardService dashboardService;
private final CalculatedFieldService calculatedFieldService; private final CalculatedFieldService calculatedFieldService;
private final DeviceProfileService deviceProfileService; private final DeviceProfileService deviceProfileService;
private final SolutionService solutionService;
@Override @Override
public InstallItemVersionResult installItemVersion(SecurityUser user, String versionId, JsonNode data) { public InstallItemVersionResult installItemVersion(SecurityUser user, String versionId, JsonNode data, HttpServletRequest request) {
TenantId tenantId = user.getTenantId(); TenantId tenantId = user.getTenantId();
log.info("[{}] Installing IoT Hub item version: {}", tenantId, versionId); log.info("[{}] Installing IoT Hub item version: {}", tenantId, versionId);
@ -99,6 +104,7 @@ public class DefaultIotHubService implements IotHubService {
case "CALCULATED_FIELD" -> installCalculatedField(user, tenantId, fileData, data); case "CALCULATED_FIELD" -> installCalculatedField(user, tenantId, fileData, data);
case "RULE_CHAIN" -> installRuleChain(tenantId, fileData); case "RULE_CHAIN" -> installRuleChain(tenantId, fileData);
case "DEVICE" -> installDeviceProfile(user, tenantId, fileData); case "DEVICE" -> installDeviceProfile(user, tenantId, fileData);
case "SOLUTION_TEMPLATE" -> installSolution(user, tenantId, fileData, request);
default -> throw new IllegalArgumentException("Unsupported IoT Hub item type: " + itemType); default -> throw new IllegalArgumentException("Unsupported IoT Hub item type: " + itemType);
}; };
@ -220,8 +226,22 @@ public class DefaultIotHubService implements IotHubService {
return new DeviceInstalledItemDescriptor(); return new DeviceInstalledItemDescriptor();
} }
private SolutionTemplateInstalledItemDescriptor installSolution(SecurityUser user, TenantId tenantId, byte[] fileData, HttpServletRequest request) throws Exception {
SolutionInstallResponse response = solutionService.installSolution(user, tenantId, fileData, request);
if (!response.isSuccess()) {
throw new RuntimeException(response.getDetails());
}
SolutionTemplateInstalledItemDescriptor descriptor = new SolutionTemplateInstalledItemDescriptor();
descriptor.setCreatedEntityIds(response.getCreatedEntityIds());
descriptor.setDashboardId(response.getDashboardId());
descriptor.setPublicId(response.getPublicId());
descriptor.setMainDashboardPublic(response.isMainDashboardPublic());
descriptor.setDetails(response.getDetails());
return descriptor;
}
@Override @Override
public UpdateItemVersionResult updateItemVersion(SecurityUser user, IotHubInstalledItemId installedItemId, String versionId, boolean force) { public UpdateItemVersionResult updateItemVersion(SecurityUser user, IotHubInstalledItemId installedItemId, String versionId, boolean force, HttpServletRequest request) {
TenantId tenantId = user.getTenantId(); TenantId tenantId = user.getTenantId();
log.info("[{}] Updating IoT Hub installed item {} to version: {}", tenantId, installedItemId, versionId); log.info("[{}] Updating IoT Hub installed item {} to version: {}", tenantId, installedItemId, versionId);
@ -231,17 +251,23 @@ public class DefaultIotHubService implements IotHubService {
throw new IllegalArgumentException("Installed item not found"); throw new IllegalArgumentException("Installed item not found");
} }
JsonNode installedVersionInfo = iotHubRestClient.getVersionInfo(installedItem.getItemVersionId().toString()); String itemType = installedItem.getItemType();
String installedChecksum = installedVersionInfo.has("checksum") ? installedVersionInfo.get("checksum").asText() : null; IotHubInstalledItemDescriptor descriptor = installedItem.getDescriptor();
log.info("[{}] Installed version info: name={}, version={}, checksum={}", tenantId, installedItem.getItemName(), installedItem.getVersion(), installedChecksum);
// Skip checksum validation for solution templates
if (!"SOLUTION_TEMPLATE".equals(itemType)) {
JsonNode installedVersionInfo = iotHubRestClient.getVersionInfo(installedItem.getItemVersionId().toString());
String installedChecksum = installedVersionInfo.has("checksum") ? installedVersionInfo.get("checksum").asText() : null;
log.info("[{}] Installed version info: name={}, version={}, checksum={}", tenantId, installedItem.getItemName(), installedItem.getVersion(), installedChecksum);
if (!force) { if (!force) {
String entityChecksum = calculateEntityChecksum(tenantId, installedItem); String entityChecksum = calculateEntityChecksum(tenantId, installedItem);
boolean entityModified = installedChecksum != null && !installedChecksum.equals(entityChecksum); boolean entityModified = installedChecksum != null && !installedChecksum.equals(entityChecksum);
log.info("[{}] Entity checksum: {}, modified: {}", tenantId, entityChecksum, entityModified); log.info("[{}] Entity checksum: {}, modified: {}", tenantId, entityChecksum, entityModified);
if (entityModified) { if (entityModified) {
return UpdateItemVersionResult.entityModified(); return UpdateItemVersionResult.entityModified();
}
} }
} }
@ -252,14 +278,25 @@ public class DefaultIotHubService implements IotHubService {
byte[] fileData = iotHubRestClient.getVersionFileData(versionId); byte[] fileData = iotHubRestClient.getVersionFileData(versionId);
log.info("[{}] Fetched update file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0); log.info("[{}] Fetched update file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0);
IotHubInstalledItemDescriptor descriptor = installedItem.getDescriptor();
String itemType = installedItem.getItemType();
switch (itemType) { switch (itemType) {
case "WIDGET" -> updateWidget(user, tenantId, (WidgetInstalledItemDescriptor) descriptor, fileData); case "WIDGET" -> updateWidget(user, tenantId, (WidgetInstalledItemDescriptor) descriptor, fileData);
case "DASHBOARD" -> updateDashboard(user, tenantId, (DashboardInstalledItemDescriptor) descriptor, fileData); case "DASHBOARD" -> updateDashboard(user, tenantId, (DashboardInstalledItemDescriptor) descriptor, fileData);
case "CALCULATED_FIELD" -> updateCalculatedField(user, tenantId, (CalculatedFieldInstalledItemDescriptor) descriptor, fileData); case "CALCULATED_FIELD" -> updateCalculatedField(user, tenantId, (CalculatedFieldInstalledItemDescriptor) descriptor, fileData);
case "RULE_CHAIN" -> updateRuleChain(tenantId, (RuleChainInstalledItemDescriptor) descriptor, fileData); case "RULE_CHAIN" -> updateRuleChain(tenantId, (RuleChainInstalledItemDescriptor) descriptor, fileData);
case "DEVICE" -> updateDeviceProfile(user, tenantId, fileData); case "DEVICE" -> updateDeviceProfile(user, tenantId, fileData);
case "SOLUTION_TEMPLATE" -> {
SolutionTemplateInstalledItemDescriptor stDescriptor = (SolutionTemplateInstalledItemDescriptor) descriptor;
solutionService.deleteSolution(tenantId, stDescriptor.getCreatedEntityIds(), user);
SolutionInstallResponse response = solutionService.installSolution(user, tenantId, fileData, request);
if (!response.isSuccess()) {
throw new RuntimeException(response.getDetails());
}
stDescriptor.setCreatedEntityIds(response.getCreatedEntityIds());
stDescriptor.setDashboardId(response.getDashboardId());
stDescriptor.setPublicId(response.getPublicId());
stDescriptor.setMainDashboardPublic(response.isMainDashboardPublic());
stDescriptor.setDetails(response.getDetails());
}
default -> throw new IllegalArgumentException("Unsupported IoT Hub item type: " + itemType); default -> throw new IllegalArgumentException("Unsupported IoT Hub item type: " + itemType);
} }
@ -470,6 +507,8 @@ public class DefaultIotHubService implements IotHubService {
} }
} else if (descriptor instanceof DeviceInstalledItemDescriptor) { } else if (descriptor instanceof DeviceInstalledItemDescriptor) {
// no entity to delete for now // no entity to delete for now
} else if (descriptor instanceof SolutionTemplateInstalledItemDescriptor st) {
solutionService.deleteSolution(tenantId, st.getCreatedEntityIds(), user);
} }
iotHubInstalledItemService.deleteById(tenantId, installedItemId); iotHubInstalledItemService.deleteById(tenantId, installedItemId);

5
application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java

@ -16,14 +16,15 @@
package org.thingsboard.server.service.iot_hub; package org.thingsboard.server.service.iot_hub;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import jakarta.servlet.http.HttpServletRequest;
import org.thingsboard.server.common.data.id.IotHubInstalledItemId; import org.thingsboard.server.common.data.id.IotHubInstalledItemId;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
public interface IotHubService { public interface IotHubService {
InstallItemVersionResult installItemVersion(SecurityUser user, String versionId, JsonNode data); InstallItemVersionResult installItemVersion(SecurityUser user, String versionId, JsonNode data, HttpServletRequest request);
UpdateItemVersionResult updateItemVersion(SecurityUser user, IotHubInstalledItemId installedItemId, String versionId, boolean force); UpdateItemVersionResult updateItemVersion(SecurityUser user, IotHubInstalledItemId installedItemId, String versionId, boolean force, HttpServletRequest request);
void deleteInstalledItem(SecurityUser user, IotHubInstalledItemId installedItemId); void deleteInstalledItem(SecurityUser user, IotHubInstalledItemId installedItemId);
} }

1462
application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java

File diff suppressed because it is too large

32
application/src/main/java/org/thingsboard/server/service/solutions/SolutionService.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions;
import jakarta.servlet.http.HttpServletRequest;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.solutions.data.solution.SolutionInstallResponse;
import java.util.List;
public interface SolutionService {
SolutionInstallResponse installSolution(SecurityUser user, TenantId tenantId, byte[] zipData, HttpServletRequest request) throws Exception;
void deleteSolution(TenantId tenantId, List<EntityId> createdEntityIds, SecurityUser user);
}

39
application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedAlarmRuleInfo.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.UUID;
public record CreatedAlarmRuleInfo(EntityId entityId, String entityName, String alarmType, String severities) {
public String getCfPageLink(UUID cfId) {
return "/alarms/alarm-rules/" + cfId;
}
public String getEntityPageLink() {
if (entityId == null) {
return null;
}
return switch (entityId.getEntityType()) {
case DEVICE_PROFILE -> "/profiles/deviceProfiles/" + entityId.getId();
case ASSET_PROFILE -> "/profiles/assetProfiles/" + entityId.getId();
default -> null;
};
}
}

39
application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedCalculatedFieldInfo.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.UUID;
public record CreatedCalculatedFieldInfo(EntityId entityId, String entityName, String type, String name) {
public String getCfPageLink(UUID cfId) {
return "/calculatedFields/" + cfId;
}
public String getEntityPageLink() {
if (entityId == null) {
return null;
}
return switch (entityId.getEntityType()) {
case DEVICE_PROFILE -> "/profiles/deviceProfiles/" + entityId.getId();
case ASSET_PROFILE -> "/profiles/assetProfiles/" + entityId.getId();
default -> null;
};
}
}

49
application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedEntityInfo.java

@ -0,0 +1,49 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CreatedEntityInfo {
private String name;
private EntityType type;
private String owner;
public String getEntityPageLink(UUID id) {
return switch (type) {
case CUSTOMER -> "/customers/" + id;
case USER -> "/users/" + id;
case ASSET -> "/entities/assets/" + id;
case DEVICE -> "/entities/devices/" + id;
case DEVICE_PROFILE -> "/profiles/deviceProfiles/" + id;
case ASSET_PROFILE -> "/profiles/assetProfiles/" + id;
case DASHBOARD -> "/dashboards/" + id;
case RULE_CHAIN -> "/ruleChains/" + id;
case EDGE -> "/edgeManagement/instances/" + id;
default -> null;
};
}
}

28
application/src/main/java/org/thingsboard/server/service/solutions/data/DashboardLinkInfo.java

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

30
application/src/main/java/org/thingsboard/server/service/solutions/data/DeviceCredentialsInfo.java

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

28
application/src/main/java/org/thingsboard/server/service/solutions/data/EdgeLinkInfo.java

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

208
application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java

@ -0,0 +1,208 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data;
import lombok.Data;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
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.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.service.solutions.data.definition.AssetDefinition;
import org.thingsboard.server.service.solutions.data.definition.AssetProfileDefinition;
import org.thingsboard.server.service.solutions.data.definition.CustomerDefinition;
import org.thingsboard.server.service.solutions.data.definition.DashboardDefinition;
import org.thingsboard.server.service.solutions.data.definition.DeviceDefinition;
import org.thingsboard.server.service.solutions.data.definition.EdgeDefinition;
import org.thingsboard.server.service.solutions.data.definition.DeviceProfileDefinition;
import org.thingsboard.server.service.solutions.data.definition.EntityDefinition;
import org.thingsboard.server.service.solutions.data.definition.EntitySearchKey;
import org.thingsboard.server.service.solutions.data.definition.EmulatorDefinition;
import org.thingsboard.server.service.solutions.data.definition.RelationDefinition;
import org.thingsboard.server.service.solutions.data.solution.TenantSolutionTemplateInstructions;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Data
public class SolutionInstallContext {
private final TenantId tenantId;
private final Path tempDir;
private final SecurityUser user;
private final TenantSolutionTemplateInstructions solutionInstructions;
private final List<EntityId> createdEntitiesList = new ArrayList<>();
private final Map<String, String> realIds = new HashMap<>();
private final Map<EntitySearchKey, EntityId> entityIdMap = new HashMap<>();
private final Map<EntityId, List<RelationDefinition>> relationDefinitions = new LinkedHashMap<>();
private final List<DashboardLinkInfo> dashboardLinks = new ArrayList<>();
// For instructions
private final Map<String, DeviceCredentialsInfo> createdDevices = new LinkedHashMap<>();
private final Map<String, UserCredentialsInfo> createdUsers = new LinkedHashMap<>();
private final Map<String, EdgeLinkInfo> createdEdges = new LinkedHashMap<>();
private final Map<UUID, CreatedEntityInfo> createdEntities = new LinkedHashMap<>();
private final Map<UUID, CreatedAlarmRuleInfo> createdAlarmRules = new LinkedHashMap<>();
private final Map<UUID, CreatedCalculatedFieldInfo> createdCalculatedFields = new LinkedHashMap<>();
private final long installTs;
private long oldestTelemetryTs;
private Map<String, EmulatorDefinition> deviceEmulators;
private Map<String, EmulatorDefinition> assetEmulators;
public SolutionInstallContext(TenantId tenantId, Path tempDir, SecurityUser user) {
this.tenantId = tenantId;
this.tempDir = tempDir;
this.user = user;
this.solutionInstructions = new TenantSolutionTemplateInstructions();
this.installTs = System.currentTimeMillis();
put(new EntitySearchKey(tenantId, EntityType.TENANT, null), tenantId);
}
public void registerReferenceOnly(String referenceId, EntityId entityId) {
if (!StringUtils.isEmpty(referenceId)) {
realIds.put(referenceId, entityId.getId().toString());
}
}
public void register(String referenceId, EntityId entityId) {
registerReferenceOnly(referenceId, entityId);
register(entityId);
}
public void register(EntityId entityId) {
createdEntitiesList.add(entityId);
}
public void register(String referenceId, RuleChain ruleChain) {
register(referenceId, ruleChain.getId());
createdEntities.put(ruleChain.getUuidId(), new CreatedEntityInfo(ruleChain.getName(), EntityType.RULE_CHAIN, "Tenant"));
}
public void register(DeviceProfileDefinition definition, DeviceProfile deviceProfile) {
register(definition.getJsonId(), deviceProfile.getId());
createdEntities.put(deviceProfile.getUuidId(), new CreatedEntityInfo(deviceProfile.getName(), EntityType.DEVICE_PROFILE, "Tenant"));
}
public void register(AssetProfileDefinition definition, AssetProfile assetProfile) {
register(definition.getJsonId(), assetProfile.getId());
createdEntities.put(assetProfile.getUuidId(), new CreatedEntityInfo(assetProfile.getName(), EntityType.ASSET_PROFILE, "Tenant"));
}
public void register(AssetDefinition definition, Asset asset) {
register(definition.getJsonId(), asset.getId());
createdEntities.put(asset.getUuidId(), new CreatedEntityInfo(asset.getName(), EntityType.ASSET,
StringUtils.isEmpty(definition.getCustomer()) ? "Tenant" : definition.getCustomer()));
}
public void register(DeviceDefinition definition, Device device) {
register(definition.getJsonId(), device.getId());
createdEntities.put(device.getUuidId(), new CreatedEntityInfo(device.getName(), EntityType.DEVICE,
StringUtils.isEmpty(definition.getCustomer()) ? "Tenant" : definition.getCustomer()));
}
public void register(CustomerDefinition definition, Customer customer) {
register(definition.getJsonId(), customer.getId());
createdEntities.put(customer.getUuidId(), new CreatedEntityInfo(customer.getName(), EntityType.CUSTOMER, "Tenant"));
}
public void register(CustomerDefinition customerDef, User user) {
register((String) null, user.getId());
createdEntities.put(user.getUuidId(), new CreatedEntityInfo(user.getName(), EntityType.USER,
StringUtils.isEmpty(customerDef.getName()) ? "Tenant" : customerDef.getName()));
}
public void register(EdgeDefinition definition, Edge edge) {
register(definition.getJsonId(), edge.getId());
createdEntities.put(edge.getUuidId(), new CreatedEntityInfo(edge.getName(), EntityType.EDGE,
StringUtils.isEmpty(definition.getCustomer()) ? "Tenant" : definition.getCustomer()));
}
public void register(DashboardDefinition definition, Dashboard dashboard) {
register(definition.getJsonId(), dashboard.getId());
createdEntities.put(dashboard.getUuidId(), new CreatedEntityInfo(dashboard.getTitle(), EntityType.DASHBOARD,
StringUtils.isEmpty(definition.getCustomer()) ? "Tenant" : definition.getCustomer()));
}
public void register(CalculatedField calculatedField) {
register(calculatedField.getId());
EntityId entityId = calculatedField.getEntityId();
CreatedEntityInfo entityInfo = createdEntities.get(entityId.getId());
boolean alarmRule = false; // TODO: CE doesn't have ALARM calculated field type yet
if (entityInfo == null) {
String target = alarmRule ? "Alarm rule" : "Calculated field";
throw new IllegalStateException("Failed to register " + target + " with name: " +
calculatedField.getName() + " for non-existing entity with id: " + entityId);
}
if (alarmRule) {
createdAlarmRules.put(calculatedField.getUuidId(), new CreatedAlarmRuleInfo(entityId, entityInfo.getName(), calculatedField.getName(), null));
return;
}
createdCalculatedFields.put(calculatedField.getUuidId(), new CreatedCalculatedFieldInfo(entityId, entityInfo.getName(), calculatedField.getType().name(), calculatedField.getName()));
}
public void putIdToMap(EntityDefinition entityDefinition, EntityId entityId) {
putIdToMap(entityDefinition.getEntityType(), entityDefinition.getName(), entityId);
}
public void putIdToMap(EntityType entityType, String entityName, EntityId entityId) {
putIdToMap(tenantId, entityType, entityName, entityId);
}
public void putIdToMap(EntityId ownerId, EntityType entityType, String entityName, EntityId entityId) {
put(new EntitySearchKey(ownerId, entityType, entityName), entityId);
}
public void put(EntitySearchKey entitySearchKey, EntityId entityId) {
entityIdMap.put(entitySearchKey, entityId);
}
@SuppressWarnings("unchecked")
public <T extends EntityId> T getIdFromMap(EntityType entityType, String entityName) {
return (T) entityIdMap.get(new EntitySearchKey(tenantId, entityType, entityName));
}
public void put(EntityId entityId, List<RelationDefinition> relations) {
relationDefinitions.put(entityId, relations);
}
public void addDeviceCredentials(DeviceCredentialsInfo deviceCredentialsInfo) {
createdDevices.put(deviceCredentialsInfo.getName(), deviceCredentialsInfo);
}
public void addUserCredentials(UserCredentialsInfo userCredentialsInfo) {
createdUsers.put(userCredentialsInfo.getName(), userCredentialsInfo);
}
public void addEdgeLinkInfo(String edgeName, EdgeLinkInfo edgeLinkInfo) {
createdEdges.put(edgeName, edgeLinkInfo);
}
}

28
application/src/main/java/org/thingsboard/server/service/solutions/data/UserCredentialsInfo.java

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

39
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/AssetDefinition.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.definition;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.EntityType;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class AssetDefinition extends CustomerEntityDefinition {
private String type;
private String label;
private String emulator;
@Override
public EntityType getEntityType() {
return EntityType.ASSET;
}
}

28
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/AssetProfileDefinition.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.definition;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.asset.AssetProfile;
@Data
@EqualsAndHashCode(callSuper = true)
public class AssetProfileDefinition extends AssetProfile {
private String jsonId;
}

48
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/BaseEntityDefinition.java

@ -0,0 +1,48 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.definition;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collections;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public abstract class BaseEntityDefinition implements EntityDefinition {
@JsonProperty("name")
private String name;
@JsonProperty("attributes")
private JsonNode attributes;
@JsonProperty("sharedAttributes")
private JsonNode sharedAttributes;
@JsonProperty("relations")
private List<RelationDefinition> relations = Collections.emptyList();
@JsonProperty("jsonId")
private String jsonId;
public void setRelations(List<RelationDefinition> relations) {
if (relations != null) {
this.relations = relations;
}
}
}

28
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CalculatedFieldDefinition.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.definition;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.cf.CalculatedField;
@Data
@EqualsAndHashCode(callSuper = true)
public class CalculatedFieldDefinition extends CalculatedField {
private Integer reprocessingOrder;
}

55
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CustomerDefinition.java

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.definition;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.service.solutions.data.names.RandomNameData;
import java.util.Collections;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
public class CustomerDefinition extends BaseEntityDefinition {
private String email;
private String country;
private String city;
private String state;
private String zip;
private String address;
private List<UserDefinition> users = Collections.emptyList();
@JsonIgnore
private RandomNameData randomNameData;
@Override
public EntityType getEntityType() {
return EntityType.CUSTOMER;
}
public void setUsers(List<UserDefinition> users) {
if (users != null) {
this.users = users;
}
}
}

28
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CustomerEntityDefinition.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.definition;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public abstract class CustomerEntityDefinition extends BaseEntityDefinition {
private String customer;
private boolean makePublic;
}

38
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DashboardDefinition.java

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.definition;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.EntityType;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class DashboardDefinition extends CustomerEntityDefinition {
private String file;
private boolean main;
@Override
public EntityType getEntityType() {
return EntityType.DASHBOARD;
}
}

26
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DashboardUserDetailsDefinition.java

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

41
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DeviceDefinition.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.definition;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.EntityType;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class DeviceDefinition extends CustomerEntityDefinition {
private String type;
private String label;
private String emulator;
private JsonNode additionalInfo;
@Override
public EntityType getEntityType() {
return EntityType.DEVICE;
}
}

28
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DeviceProfileDefinition.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.definition;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.DeviceProfile;
@Data
@EqualsAndHashCode(callSuper = true)
public class DeviceProfileDefinition extends DeviceProfile {
private String jsonId;
}

45
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EdgeDefinition.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.definition;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.EntityType;
import java.util.Collections;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class EdgeDefinition extends CustomerEntityDefinition {
private String type;
private String label;
private String rootRuleChainId;
private List<String> ruleChainIds = Collections.emptyList();
private List<String> deviceIds = Collections.emptyList();
private List<String> assetIds = Collections.emptyList();
@Override
public EntityType getEntityType() {
return EntityType.EDGE;
}
}

64
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EmulatorDefinition.java

@ -0,0 +1,64 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.definition;
import lombok.Data;
import org.thingsboard.server.common.data.StringUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
@Data
public class EmulatorDefinition {
private String name;
private String extendz;
private String clazz;
private int publishPeriodInDays;
private int publishFrequencyInSeconds;
private int publishPauseInMillis;
private long activityPeriodInMillis;
private List<TelemetryProfile> telemetryProfiles = Collections.emptyList();
public void enrich(EmulatorDefinition parent) {
if (StringUtils.isEmpty(clazz)) {
clazz = parent.getClazz();
}
if (publishPeriodInDays == 0) {
publishPeriodInDays = parent.getPublishPeriodInDays();
}
if (publishFrequencyInSeconds == 0) {
publishFrequencyInSeconds = parent.getPublishFrequencyInSeconds();
}
if (publishPauseInMillis == 0) {
publishPauseInMillis = parent.getPublishPauseInMillis();
}
if (activityPeriodInMillis == 0L) {
activityPeriodInMillis = parent.getActivityPeriodInMillis();
}
var profilesMap = telemetryProfiles.stream().collect(Collectors.toMap(TelemetryProfile::getKey, Function.identity()));
parent.getTelemetryProfiles().forEach(tp -> profilesMap.putIfAbsent(tp.getKey(), tp));
telemetryProfiles = new ArrayList<>(profilesMap.values());
}
public long getOldestTs(long startTs) {
return startTs - TimeUnit.DAYS.toMillis(publishPeriodInDays) - publishFrequencyInSeconds;
}
}

26
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EntityDefinition.java

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

31
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EntitySearchKey.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.definition;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
@Data
@AllArgsConstructor
public class EntitySearchKey {
private final EntityId ownerId;
private final EntityType entityType;
private final String entityName;
}

32
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/ReferenceableEntityDefinition.java

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

34
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/RelationDefinition.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.definition;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RelationDefinition {
private EntityType entityType;
private String entityName;
private EntitySearchDirection direction;
private String type;
}

29
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/TelemetryProfile.java

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

29
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/TenantDefinition.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.definition;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
@Data
public class TenantDefinition extends BaseEntityDefinition {
@Override
public EntityType getEntityType() {
return EntityType.TENANT;
}
}

40
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/UserDefinition.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.definition;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.EntityType;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class UserDefinition extends BaseEntityDefinition {
private String firstname;
private String lastname;
private String password;
private DashboardUserDetailsDefinition dashboard;
@Override
public EntityType getEntityType() {
return EntityType.USER;
}
}

195
application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/AbstractEmulatorLauncher.java

@ -0,0 +1,195 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.emulator;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
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.service.solutions.data.definition.EmulatorDefinition;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Data
public abstract class AbstractEmulatorLauncher<T extends HasId<? extends EntityId> & HasTenantId & HasName> {
protected static final TbQueueCallback EMPTY_CALLBACK = new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
}
@Override
public void onFailure(Throwable t) {
}
};
protected final T entity;
protected final EmulatorDefinition emulatorDefinition;
protected final ExecutorService oldTelemetryExecutor;
protected final TbClusterService tbClusterService;
protected final PartitionService partitionService;
protected final TbQueueProducerProvider tbQueueProducerProvider;
protected final TbServiceInfoProvider serviceInfoProvider;
protected final TelemetrySubscriptionService tsSubService;
protected final long publishFrequency;
private final Emulator emulator;
private ScheduledFuture<?> scheduledFuture;
public AbstractEmulatorLauncher(T entity,
EmulatorDefinition emulatorDefinition,
ExecutorService oldTelemetryExecutor,
TbClusterService tbClusterService,
PartitionService partitionService,
TbQueueProducerProvider tbQueueProducerProvider,
TbServiceInfoProvider serviceInfoProvider,
TelemetrySubscriptionService tsSubService) throws Exception {
this.entity = entity;
this.emulatorDefinition = emulatorDefinition;
this.oldTelemetryExecutor = oldTelemetryExecutor;
this.tbClusterService = tbClusterService;
this.partitionService = partitionService;
this.tbQueueProducerProvider = tbQueueProducerProvider;
this.serviceInfoProvider = serviceInfoProvider;
this.tsSubService = tsSubService;
this.publishFrequency = TimeUnit.SECONDS.toMillis(Math.max(emulatorDefinition.getPublishFrequencyInSeconds(), 1));
if (StringUtils.isEmpty(emulatorDefinition.getClazz())) {
emulator = new BasicEmulator();
} else {
emulator = (Emulator) Class.forName(emulatorDefinition.getClazz()).getDeclaredConstructor().newInstance();
}
emulator.init(emulatorDefinition);
}
public CompletableFuture<Void> launch() {
final long oldestTs = emulatorDefinition.getOldestTs(System.currentTimeMillis());
CompletableFuture<Void> future = new CompletableFuture<>();
oldTelemetryExecutor.submit(() -> {
AtomicInteger pending = new AtomicInteger(1);
try {
if (emulator instanceof SimpleEmulator) {
if (oldestTs < (System.currentTimeMillis() - publishFrequency)) {
pushOldTelemetry(oldestTs, pending, future);
}
} else if (emulator instanceof CustomEmulator customEmulator) {
Pair<Long, ObjectNode> telemetry = customEmulator.getNextValue();
while (telemetry != null) {
pending.incrementAndGet();
publishTelemetry(telemetry.getFirst(), telemetry.getSecond(), pending, future);
telemetry = customEmulator.getNextValue();
}
}
} catch (Exception e) {
log.warn("Telemetry upload failed for {}", entity.getName(), e);
future.completeExceptionally(e);
} finally {
if (pending.decrementAndGet() == 0 && !future.isDone()) {
log.trace("[{}] Telemetry emulation finished. Producer and all callbacks completed successfully", entity.getName());
future.complete(null);
}
}
future.whenComplete((v, t) -> {
try {
postProcessEntity(entity);
} catch (Exception e) {
log.warn("Post-processing failed for {}", entity.getName(), e);
}
});
});
return future;
}
protected void postProcessEntity(T entity) {
}
private void pushOldTelemetry(long oldestTs, AtomicInteger pending, CompletableFuture<Void> future) throws InterruptedException {
for (long ts = oldestTs; ts < System.currentTimeMillis(); ts += publishFrequency) {
pending.incrementAndGet();
publishTelemetry(ts, pending, future);
}
}
private void publishTelemetry(long ts, AtomicInteger pending, CompletableFuture<Void> future) throws InterruptedException {
ObjectNode values = ((SimpleEmulator) emulator).getValue(ts);
publishTelemetry(ts, values, pending, future);
Thread.sleep(emulatorDefinition.getPublishPauseInMillis());
}
public void stop() {
scheduledFuture.cancel(true);
}
private void publishTelemetry(long ts, ObjectNode value, AtomicInteger pending, CompletableFuture<Void> future) {
String msgData = JacksonUtil.toString(value);
log.debug("[{}] Publishing telemetry: {}", entity.getName(), msgData);
TbMsgMetaData md = new TbMsgMetaData();
md.putValue("ts", Long.toString(ts));
TbMsg tbMsg = TbMsg.newMsg()
.type(TbMsgType.POST_TELEMETRY_REQUEST)
.originator(entity.getId())
.copyMetaData(md)
.dataType(TbMsgDataType.JSON)
.data(msgData)
.build();
TbQueueCallback callback = new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
log.debug("[{}] Successfully pushed message to Rule Engine for ts {}", entity.getName(), ts);
if (pending.decrementAndGet() == 0 && !future.isDone()) {
log.trace("[{}] Completing emulation future from callback for msg with ts: {}", entity.getName(), ts);
future.complete(null);
}
}
@Override
public void onFailure(Throwable t) {
log.warn("[{}] Telemetry upload failed for ts {}", entity.getName(), ts, t);
if (pending.decrementAndGet() == 0 && !future.isDone()) {
future.completeExceptionally(t);
}
}
};
tbClusterService.pushMsgToRuleEngine(entity.getTenantId(), entity.getId(), tbMsg, callback);
}
}

42
application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/AssetEmulatorLauncher.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.emulator;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.asset.Asset;
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.service.solutions.data.definition.EmulatorDefinition;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.concurrent.ExecutorService;
@Slf4j
public class AssetEmulatorLauncher extends AbstractEmulatorLauncher<Asset> {
@Builder
public AssetEmulatorLauncher(Asset entity, EmulatorDefinition emulatorDefinition, ExecutorService oldTelemetryExecutor, TbClusterService tbClusterService,
PartitionService partitionService,
TbQueueProducerProvider tbQueueProducerProvider,
TbServiceInfoProvider serviceInfoProvider,
TelemetrySubscriptionService tsSubService) throws Exception {
super(entity, emulatorDefinition, oldTelemetryExecutor, tbClusterService, partitionService, tbQueueProducerProvider, serviceInfoProvider, tsSubService);
}
}

42
application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/BasicEmulator.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.emulator;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.service.solutions.data.definition.EmulatorDefinition;
import org.thingsboard.server.service.solutions.data.values.TelemetryGenerator;
import org.thingsboard.server.service.solutions.data.values.TelemetryGeneratorFactory;
import java.util.HashMap;
import java.util.Map;
public class BasicEmulator implements SimpleEmulator {
private final Map<String, TelemetryGenerator> tsGenerators = new HashMap<>();
@Override
public void init(EmulatorDefinition emulatorDefinition) {
emulatorDefinition.getTelemetryProfiles().forEach(tp -> tsGenerators.put(tp.getKey(), TelemetryGeneratorFactory.create(tp)));
}
@Override
public ObjectNode getValue(long ts) {
ObjectNode values = JacksonUtil.newObjectNode();
tsGenerators.values().forEach(gen -> gen.addValue(ts, values));
return values;
}
}

25
application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/CustomEmulator.java

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.emulator;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.data.util.Pair;
public interface CustomEmulator extends Emulator {
Pair<Long, ObjectNode> getNextValue();
}

98
application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/DeviceEmulatorLauncher.java

@ -0,0 +1,98 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.emulator;
import com.google.common.util.concurrent.FutureCallback;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
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.service.solutions.data.definition.EmulatorDefinition;
import org.thingsboard.server.service.state.DefaultDeviceStateService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
@Slf4j
public class DeviceEmulatorLauncher extends AbstractEmulatorLauncher<Device> {
@Builder
public DeviceEmulatorLauncher(Device entity,
EmulatorDefinition emulatorDefinition,
ExecutorService oldTelemetryExecutor,
TbClusterService tbClusterService,
PartitionService partitionService,
TbQueueProducerProvider tbQueueProducerProvider,
TbServiceInfoProvider serviceInfoProvider,
TelemetrySubscriptionService tsSubService) throws Exception {
super(entity, emulatorDefinition, oldTelemetryExecutor, tbClusterService, partitionService, tbQueueProducerProvider, serviceInfoProvider, tsSubService);
}
@Override
protected void postProcessEntity(Device entity) {
if (this.emulatorDefinition.getActivityPeriodInMillis() > 0) {
tsSubService.saveAttributes(AttributesSaveRequest.builder()
.tenantId(entity.getTenantId())
.entityId(entity.getId())
.scope(AttributeScope.SERVER_SCOPE)
.entry(new LongDataEntry(DefaultDeviceStateService.INACTIVITY_TIMEOUT, this.emulatorDefinition.getActivityPeriodInMillis()))
.callback(new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Void unused) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, entity.getTenantId(), entity.getId());
UUID sessionId = UUID.randomUUID();
TransportProtos.TransportToDeviceActorMsg msg = TransportProtos.TransportToDeviceActorMsg.newBuilder()
.setSessionInfo(TransportProtos.SessionInfoProto.newBuilder()
.setSessionIdMSB(sessionId.getMostSignificantBits())
.setSessionIdLSB(sessionId.getLeastSignificantBits())
.setDeviceIdMSB(entity.getId().getId().getMostSignificantBits())
.setDeviceIdLSB(entity.getId().getId().getLeastSignificantBits())
.setDeviceProfileIdMSB(entity.getId().getId().getMostSignificantBits())
.setDeviceProfileIdLSB(entity.getId().getId().getLeastSignificantBits())
.setDeviceName(entity.getName())
.setDeviceType(entity.getType())
.setTenantIdMSB(entity.getTenantId().getId().getMostSignificantBits())
.setTenantIdLSB(entity.getTenantId().getId().getLeastSignificantBits())
.setNodeId(serviceInfoProvider.getServiceId())
.build())
.setSubscriptionInfo(TransportProtos.SubscriptionInfoProto.newBuilder().setLastActivityTime(System.currentTimeMillis()).build())
.build();
tbQueueProducerProvider.getTbCoreMsgProducer().send(tpi,
new TbProtoQueueMsg<>(entity.getUuidId(),
TransportProtos.ToCoreMsg.newBuilder().setToDeviceActorMsg(msg).build()), EMPTY_CALLBACK
);
}
@Override
public void onFailure(Throwable throwable) {
}
})
.build());
}
}
}

24
application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/Emulator.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.emulator;
import org.thingsboard.server.service.solutions.data.definition.EmulatorDefinition;
public interface Emulator {
void init(EmulatorDefinition emulatorDefinition);
}

108
application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/FieldIrrigationStateEmulator.java

@ -0,0 +1,108 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.emulator;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.service.solutions.data.definition.EmulatorDefinition;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.TimeZone;
public class FieldIrrigationStateEmulator implements CustomEmulator {
private static final int START_HOUR = 6;
private int idx;
private List<Pair<Long, ObjectNode>> data = new ArrayList<>();
@Override
public void init(EmulatorDefinition emulatorDefinition) {
idx = 0;
var tz = TimeZone.getTimeZone("America/New_York");
Calendar c = Calendar.getInstance();
c.setTimeInMillis(System.currentTimeMillis());
c.setTimeZone(tz);
int curHour = c.get(Calendar.HOUR_OF_DAY);
c.add(Calendar.DAY_OF_MONTH, curHour < START_HOUR ? -7 : -6);
c.set(Calendar.HOUR_OF_DAY, START_HOUR);
c.set(Calendar.MINUTE, 0);
add(c, JacksonUtil.newObjectNode()
.put("startTs", c.getTimeInMillis())
.put("durationThreshold", 30 * 60000)
.put("consumption", 423)
.put("duration", 30 * 60000)
);
add(c, JacksonUtil.newObjectNode()
.put("startTs", c.getTimeInMillis())
.put("durationThreshold", 30 * 60000)
.put("consumption", 407)
.put("duration", 30 * 60000)
);
add(c, JacksonUtil.newObjectNode()
.put("startTs", c.getTimeInMillis())
.put("consumptionThreshold", 1000)
.put("consumption", 1000)
.put("duration", 63 * 60000)
);
add(c, JacksonUtil.newObjectNode()
.put("startTs", c.getTimeInMillis())
.put("consumptionThreshold", 500)
.put("consumption", 500)
.put("duration", 36 * 60000)
);
add(c, JacksonUtil.newObjectNode()
.put("startTs", c.getTimeInMillis())
.put("consumptionThreshold", 1000)
.put("consumption", 1000)
.put("duration", 63 * 60000)
);
add(c, JacksonUtil.newObjectNode()
.put("startTs", c.getTimeInMillis())
.put("durationThreshold", 30 * 60000)
.put("duration", 30 * 60000)
.put("consumption", 452)
);
add(c, JacksonUtil.newObjectNode()
.put("startTs", c.getTimeInMillis())
.put("durationThreshold", 30 * 60000)
.put("duration", 30 * 60000)
.put("consumption", 447)
);
}
private void add(Calendar c, ObjectNode objectNode) {
ObjectNode startIrrigationMsg = JacksonUtil.newObjectNode();
startIrrigationMsg.put("irrigationState", "DONE");
startIrrigationMsg.set("irrigationTask", objectNode);
data.add(Pair.of(c.getTimeInMillis(), startIrrigationMsg));
c.add(Calendar.DAY_OF_MONTH, 1);
}
@Override
public Pair<Long, ObjectNode> getNextValue() {
if (idx < data.size()) {
var result = data.get(idx);
idx++;
return result;
} else {
return null;
}
}
}

24
application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/SimpleEmulator.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.emulator;
import com.fasterxml.jackson.databind.node.ObjectNode;
public interface SimpleEmulator extends Emulator {
ObjectNode getValue(long ts);
}

26
application/src/main/java/org/thingsboard/server/service/solutions/data/names/RandomNameData.java

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

100
application/src/main/java/org/thingsboard/server/service/solutions/data/names/RandomNameUtil.java

@ -0,0 +1,100 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.names;
import org.thingsboard.server.common.data.StringUtils;
import java.util.Random;
public class RandomNameUtil {
private static final Random RANDOM = new Random(System.currentTimeMillis());
private static String[] FIRST_NAMES = new String[]{
"Nadia", "Lynne", "Lera", "Holly", "Zina", "Mandi", "Kasie", "Josef", "Liane", "Man",
"Karan", "Nga", "Tesha", "Elva", "Bree", "Ida", "Mimi", "Altha", "Omar", "Ollie",
"Nicol", "Ned", "Teri", "Sabra", "Kari", "Eva", "Penni", "Simon", "Kaci", "Jess",
"Bea", "Ron", "Melba", "Vella", "Nada", "Cyndi", "Audry", "Helga", "Chana", "Lola",
"Terry", "Tami", "Dedra", "Erwin", "Anne", "Zoila", "Nelia", "Hyman", "Deane", "Erica",
"Amos", "Maura", "Gwenn", "Evan", "Lelia", "Grant", "Abe", "Fanny", "Cindi", "Pilar",
"Darcy", "Len", "Casie", "Jose", "Kylie", "Cami", "Casey", "Kerri", "Bruno", "Theda",
"Ardis", "Carin", "Belia", "Jeff", "Yetta", "Ola", "Lyla", "Megan", "Zita", "Rocky",
"Darci", "Dale", "Mirta", "Tanja", "Stacy", "Julie", "Aisha", "Avis", "Hugh", "Anh",
"Bud", "Earle", "Ossie", "Odell", "Dovie", "Afton", "Joel", "Aida", "Vina", "Emiko",
"Tori", "Keena", "Addie", "Nancy", "Tonda", "Josue", "Dina", "Mitch", "Thea", "Cole",
"Lyman", "Donna", "Roma", "Deena", "Lue", "Bette", "Ilene", "Vera", "Kirby", "Lenna",
"Pat", "Grace", "Gus", "Frank", "Lena", "Adele", "Kerry", "Santo", "Wade", "Trudi",
"Bruce", "Raul", "Katy", "Heidi", "Marna", "Faith", "Ronna", "Kylee", "Emma", "Luna",
"Ilda", "Mose", "Juan", "Cori", "Gayla", "Otha", "Jung", "Bambi", "Joi", "Sibyl",
"Reid", "Bonny", "Roni", "Joy", "Farah", "Jeane", "Jill", "Tisha", "Leon", "Leigh",
"Inge", "Zella", "Rubin", "Carri", "Nana", "Sofia", "Lucio", "Eboni", "Adam", "Lino",
"Elvis", "Marci", "Adina", "Nanci", "Joyce", "James", "Louis", "Aurea", "Chi", "Kathy",
"Nina", "Lise", "Reda", "Candi", "Ralph", "Velva", "Mari", "Mavis", "Kory", "Tammi",
"Lucy", "Lavon", "Ana", "Lin", "Alena", "Haley", "Myra", "Amy", "Marlo", "Ami"
};
private static String[] LAST_NAMES = new String[]{
"Vinson", "Rogers", "Burkett", "Gamboa", "Gross", "Toledo", "Kiser", "Harman", "Pierce", "Lovett",
"Sexton", "Coates", "Seymour", "Holland", "Finley", "Hagen", "Schmitt", "Beach", "Rea", "Peck",
"Noel", "Archer", "Zamora", "Wood", "Rivera", "Lentz", "Alonso", "Rider", "Story", "Schwab",
"Mercado", "Caruso", "Bynum", "Spears", "Bingham", "Tyler", "Ponce", "Skaggs", "Matos", "Stinson",
"Burgos", "Coles", "Helton", "Joyce", "Galvan", "Grant", "Stahl", "Daigle", "Hayden", "Quiroz",
"Horne", "Shaw", "Addison", "Harvey", "Watkins", "Romano", "Ahmed", "Lind", "Swenson", "Kemp",
"Barnes", "Horton", "Dugan", "Farr", "Swanson", "Hale", "Hidalgo", "Schulte", "Ervin", "Osorio",
"Lincoln", "Zhang", "Oakes", "Stiles", "Lu", "Culver", "Singer", "Knott", "Bullard", "Butts",
"Coley", "Roth", "Cobb", "Darnell", "Vaughan", "Conrad", "Austin", "Rico", "Hobson", "Brunner",
"Mccray", "Mattson", "Barber", "Clement", "Ly", "Dejesus", "Painter", "Macias", "Healy", "Duncan",
"Forrest", "Hurley", "Torres", "Blue", "Rucker", "Ferrell", "Maddox", "Osborne", "Holt", "Briggs",
"Irvin", "Bassett", "Urban", "Overton", "Sloan", "Guzman", "Estrada", "Redding", "Gold", "Paz",
"Lucero", "Reyes", "Conner", "Keith", "Hays", "Hutton", "Moore", "Tuttle", "Powers", "William",
"Reeder", "Castro", "Vela", "Baca", "Frazier", "Kline", "Huggins", "Zepeda", "Pate", "Marion",
"Hollis", "Ybarra", "Cuellar", "Gallo", "Gomes", "Louis", "Bolden", "Liu", "Hayes", "Simpson",
"Otero", "Conway", "Peoples", "Mohr", "Loomis", "Engel", "Hughes", "Chapman", "Bower", "Schmidt",
"Mcmahon", "Booth", "Ward", "Drake", "Cutler", "Gordon", "Black", "Clifton", "Curran", "Pierre",
"Workman", "Tilley", "Moody", "Kuhn", "Eastman", "Scott", "Vang", "Tillman", "Cleary", "Yi",
"Griffin", "Mendoza", "Mason", "Pittman", "Landis", "Gay", "Rowland", "Jordan", "Lo", "Kumar",
"Vernon", "Boyd", "Rocha", "Mcqueen", "Cornett", "Deaton", "Borden", "Lacey", "Gaston", "Miles"
};
public static String nextFirstName() {
return randomElement(FIRST_NAMES);
}
public static String nextLastName() {
return randomElement(LAST_NAMES);
}
private static String toEmail(String firstName, String lastName) {
return firstName.toLowerCase() + "." + lastName.toLowerCase() + "@thingsboard.io";
}
public static RandomNameData next() {
var firstName = nextFirstName();
var lastName = nextLastName();
return new RandomNameData(firstName, lastName, toEmail(firstName, lastName));
}
public static RandomNameData nextSuperRandom() {
var firstName = nextFirstName() + StringUtils.randomAlphanumeric(10).toLowerCase();
var lastName = nextLastName() + StringUtils.randomAlphanumeric(10).toLowerCase();
return new RandomNameData(firstName, lastName, toEmail(firstName, lastName));
}
private static String randomElement(String[] array) {
return array[RANDOM.nextInt(array.length)];
}
}

42
application/src/main/java/org/thingsboard/server/service/solutions/data/solution/SolutionInstallResponse.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.solution;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.List;
@Schema
@Data
public class SolutionInstallResponse extends TenantSolutionTemplateInstructions {
@Schema(description = "Indicates that template was installed successfully")
private boolean success;
@Schema(description = "List of entity IDs created during solution installation")
private List<EntityId> createdEntityIds;
public SolutionInstallResponse(TenantSolutionTemplateInstructions instructions, boolean success, List<EntityId> createdEntityIds) {
super(instructions);
this.success = success;
this.createdEntityIds = createdEntityIds;
}
public SolutionInstallResponse() {
super();
}
}

46
application/src/main/java/org/thingsboard/server/service/solutions/data/solution/TenantSolutionTemplateInstructions.java

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.solution;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
@Schema
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TenantSolutionTemplateInstructions {
@Schema(description = "Id of the main dashboard of the solution")
private DashboardId dashboardId;
@Schema(description = "Id of the public customer if solution has public entities")
private CustomerId publicId;
@Schema(description = "Is the main dashboard public")
private boolean mainDashboardPublic;
@Schema(description = "Markdown with solution usage instructions")
private String details;
public TenantSolutionTemplateInstructions(TenantSolutionTemplateInstructions instructions) {
this.dashboardId = instructions.getDashboardId();
this.publicId = instructions.getPublicId();
this.mainDashboardPublic = instructions.isMainDashboardPublic();
this.details = instructions.getDetails();
}
}

34
application/src/main/java/org/thingsboard/server/service/solutions/data/values/CompositeValueStrategyDefinition.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class CompositeValueStrategyDefinition implements ValueStrategyDefinition {
private ValueStrategyDefinition defaultHours;
private ValueStrategyDefinition workHours;
private ValueStrategyDefinition nightHours;
private ValueStrategyDefinition holidayHours;
@Override
public ValueStrategyDefinitionType getStrategyType() {
return ValueStrategyDefinitionType.COMPOSITE;
}
}

55
application/src/main/java/org/thingsboard/server/service/solutions/data/values/CompositeValueStrategyGenerator.java

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile;
public class CompositeValueStrategyGenerator extends TelemetryGenerator {
TelemetryGenerator defaultGenerator;
TelemetryGenerator whGenerator;
TelemetryGenerator nhGenerator;
TelemetryGenerator hhGenerator;
public CompositeValueStrategyGenerator(TelemetryProfile tp) {
super(tp);
var def = (CompositeValueStrategyDefinition) tp.getValueStrategy();
defaultGenerator = TelemetryGeneratorFactory.create(new TelemetryProfile(tp.getKey(), def.getDefaultHours()));
if (def.getWorkHours() != null) {
whGenerator = TelemetryGeneratorFactory.create(new TelemetryProfile(tp.getKey(), def.getWorkHours()));
}
if (def.getNightHours() != null) {
nhGenerator = TelemetryGeneratorFactory.create(new TelemetryProfile(tp.getKey(), def.getNightHours()));
}
if (def.getHolidayHours() != null) {
hhGenerator = TelemetryGeneratorFactory.create(new TelemetryProfile(tp.getKey(), def.getHolidayHours()));
}
}
@Override
public void addValue(long ts, ObjectNode values) {
if (hhGenerator != null && GeneratorTools.isHoliday(ts)) {
hhGenerator.addValue(ts, values);
} else if (nhGenerator != null && GeneratorTools.isNightHour(ts)) {
nhGenerator.addValue(ts, values);
} else if (whGenerator != null && GeneratorTools.isWorkHour(ts)) {
whGenerator.addValue(ts, values);
} else {
defaultGenerator.addValue(ts, values);
}
}
}

51
application/src/main/java/org/thingsboard/server/service/solutions/data/values/ConstantTelemetryGenerator.java

@ -0,0 +1,51 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile;
public class ConstantTelemetryGenerator extends TelemetryGenerator {
private final ConstantValueStrategyDefinition strategy;
private JsonNode value;
public ConstantTelemetryGenerator(TelemetryProfile telemetryProfile) {
super(telemetryProfile);
this.strategy = (ConstantValueStrategyDefinition) telemetryProfile.getValueStrategy();
this.value = strategy.getValue();
}
@Override
public void addValue(long ts, ObjectNode values) {
values.set(key, value);
}
@Override
public double getValue() {
if (value != null && value.isNumber()) {
return value.doubleValue();
} else {
return super.getValue();
}
}
@Override
public void setValue(double value) {
// do nothing;
}
}

31
application/src/main/java/org/thingsboard/server/service/solutions/data/values/ConstantValueStrategyDefinition.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ConstantValueStrategyDefinition implements ValueStrategyDefinition {
private JsonNode value;
@Override
public ValueStrategyDefinitionType getStrategyType() {
return ValueStrategyDefinitionType.CONSTANT;
}
}

69
application/src/main/java/org/thingsboard/server/service/solutions/data/values/CounterTelemetryGenerator.java

@ -0,0 +1,69 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter;
import lombok.Setter;
import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile;
import java.math.BigDecimal;
import java.math.RoundingMode;
import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.getMultiplier;
import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.randomDouble;
public class CounterTelemetryGenerator extends TelemetryGenerator {
private final CounterValueStrategyDefinition strategy;
@Getter @Setter
private double value;
public CounterTelemetryGenerator(TelemetryProfile telemetryProfile) {
super(telemetryProfile);
this.strategy = (CounterValueStrategyDefinition) telemetryProfile.getValueStrategy();
this.value = getRandomStartValue();
}
@Override
public void addValue(long ts, ObjectNode values) {
double step = randomDouble(strategy.getMinIncrement(), strategy.getMaxIncrement());
double multiplier = getMultiplier(ts, strategy.getHolidayMultiplier(), strategy.getWorkHoursMultiplier(), strategy.getNightHoursMultiplier());
value += step * multiplier;
if (value > getRandomEndValue()) {
value = getRandomStartValue();
}
put(values, value);
}
private void put(ObjectNode values, double value) {
if (strategy.getPrecision() == 0) {
values.put(key, (int) value);
} else {
values.put(key, BigDecimal.valueOf(value)
.setScale(strategy.getPrecision(), RoundingMode.HALF_UP)
.doubleValue());
}
}
public double getRandomStartValue() {
return randomDouble(strategy.getMinStartValue(), strategy.getMaxStartValue());
}
public double getRandomEndValue() {
return randomDouble(strategy.getMinEndValue(), strategy.getMaxEndValue());
}
}

42
application/src/main/java/org/thingsboard/server/service/solutions/data/values/CounterValueStrategyDefinition.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class CounterValueStrategyDefinition implements ValueStrategyDefinition {
private int precision;
private double minStartValue;
private double maxStartValue;
private double minEndValue;
private double maxEndValue;
private double minIncrement;
private double maxIncrement;
private double holidayMultiplier;
private double workHoursMultiplier;
private double nightHoursMultiplier;
@Override
public ValueStrategyDefinitionType getStrategyType() {
return ValueStrategyDefinitionType.COUNTER;
}
}

37
application/src/main/java/org/thingsboard/server/service/solutions/data/values/DecrementTelemetryGenerator.java

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile;
import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.randomDouble;
public class DecrementTelemetryGenerator extends IncDecTelemetryGenerator<DecrementValueStrategyDefinition> {
public DecrementTelemetryGenerator(TelemetryProfile telemetryProfile) {
super(telemetryProfile);
}
@Override
public void addValue(long ts, ObjectNode values) {
double step = randomDouble(strategy.getMinDecrement(), strategy.getMaxDecrement());
double newValue = value - step;
value = Math.max(newValue, endValue);
put(values, value);
}
}

34
application/src/main/java/org/thingsboard/server/service/solutions/data/values/DecrementValueStrategyDefinition.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DecrementValueStrategyDefinition extends IncDecValueStrategyDefinition {
private double minDecrement;
private double maxDecrement;
@Override
public ValueStrategyDefinitionType getStrategyType() {
return ValueStrategyDefinitionType.DECREMENT;
}
}

54
application/src/main/java/org/thingsboard/server/service/solutions/data/values/EventTelemetryGenerator.java

@ -0,0 +1,54 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile;
import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.randomLong;
public class EventTelemetryGenerator extends TelemetryGenerator {
private final EventValueStrategyDefinition strategy;
private final long currentAnomaly;
private Object value;
public EventTelemetryGenerator(TelemetryProfile telemetryProfile) {
super(telemetryProfile);
this.strategy = (EventValueStrategyDefinition) telemetryProfile.getValueStrategy();
currentAnomaly = randomLong(0, strategy.getAnomalyChance());
}
@Override
public void addValue(long ts, ObjectNode values) {
boolean anomaly = false;
if (randomLong(0, strategy.getAnomalyChance()) == currentAnomaly) {
anomaly = true;
}
this.value = anomaly ? strategy.getAnomalyValue() : strategy.getNormalValue();
if (value instanceof Boolean) {
values.put(key, (Boolean) value);
} else if (value instanceof Double) {
values.put(key, ((Double) value));
} else if (value instanceof Integer) {
values.put(key, ((Integer) value));
} else {
throw new RuntimeException("Not supported value for event telemetry generator: " + value);
}
}
}

33
application/src/main/java/org/thingsboard/server/service/solutions/data/values/EventValueStrategyDefinition.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class EventValueStrategyDefinition implements ValueStrategyDefinition {
private Object normalValue;
private Object anomalyValue;
private double anomalyChance;
@Override
public ValueStrategyDefinitionType getStrategyType() {
return ValueStrategyDefinitionType.EVENT;
}
}

96
application/src/main/java/org/thingsboard/server/service/solutions/data/values/GeneratorTools.java

@ -0,0 +1,96 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import java.util.Calendar;
import java.util.Date;
import java.util.Random;
import java.util.TimeZone;
public class GeneratorTools {
private static final Random random = new Random();
public static long randomLong(double min, double max) {
return (long) randomDouble(min, max);
}
public static double randomDouble(double min, double max) {
return min + random.nextDouble() * Math.abs(max - min);
}
public static double getMultiplier(long ts, double holidayMultiplier, double workHoursMultiplier, double nightHoursMultiplier) {
Date date = new Date(ts);
Calendar c = Calendar.getInstance();
c.setTime(date);
int dayOfWeek = c.get(Calendar.DAY_OF_WEEK);
int hour = c.get(Calendar.HOUR_OF_DAY);
double multiplier = 1.0;
if (dayOfWeek == 1 || dayOfWeek == 7) {
multiplier *= holidayMultiplier;
}
if (hour > 8 && hour < 18) {
multiplier *= workHoursMultiplier;
} else if (hour < 6 || hour > 22) {
multiplier *= nightHoursMultiplier;
}
return multiplier;
}
public static int getHour(TimeZone tz, long ts) {
Date date = new Date(ts);
Calendar c = Calendar.getInstance();
c.setTimeZone(tz);
c.setTime(date);
return c.get(Calendar.HOUR_OF_DAY);
}
public static int getMinute(TimeZone tz, long ts) {
Date date = new Date(ts);
Calendar c = Calendar.getInstance();
c.setTimeZone(tz);
c.setTime(date);
return c.get(Calendar.MINUTE);
}
public static boolean isHoliday(long ts) {
Date date = new Date(ts);
Calendar c = Calendar.getInstance();
c.setTime(date);
int dayOfWeek = c.get(Calendar.DAY_OF_WEEK);
return dayOfWeek == 1 || dayOfWeek == 7;
}
public static boolean isWorkHour(long ts) {
Date date = new Date(ts);
Calendar c = Calendar.getInstance();
c.setTime(date);
int hour = c.get(Calendar.HOUR_OF_DAY);
return hour > 8 && hour < 18;
}
public static boolean isNightHour(long ts) {
Date date = new Date(ts);
Calendar c = Calendar.getInstance();
c.setTime(date);
int hour = c.get(Calendar.HOUR_OF_DAY);
return hour < 6 || hour >= 22;
}
}

64
application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncDecTelemetryGenerator.java

@ -0,0 +1,64 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter;
import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile;
import java.math.BigDecimal;
import java.math.RoundingMode;
import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.randomDouble;
public abstract class IncDecTelemetryGenerator<T extends IncDecValueStrategyDefinition> extends TelemetryGenerator {
protected final T strategy;
@Getter
protected double value;
protected double endValue;
@SuppressWarnings("unchecked")
public IncDecTelemetryGenerator(TelemetryProfile telemetryProfile) {
super(telemetryProfile);
this.strategy = (T) telemetryProfile.getValueStrategy();
this.value = getRandomStartValue();
this.endValue = getRandomEndValue();
}
public void setValue(double value) {
this.value = value;
this.endValue = getRandomEndValue();
}
protected void put(ObjectNode values, double value) {
if (strategy.getPrecision() == 0) {
values.put(key, (int) value);
} else {
values.put(key, BigDecimal.valueOf(value)
.setScale(strategy.getPrecision(), RoundingMode.HALF_UP)
.doubleValue());
}
}
public double getRandomStartValue() {
return randomDouble(strategy.getMinStartValue(), strategy.getMaxStartValue());
}
public double getRandomEndValue() {
return randomDouble(strategy.getMinEndValue(), strategy.getMaxEndValue());
}
}

32
application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncDecValueStrategyDefinition.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public abstract class IncDecValueStrategyDefinition implements ValueStrategyDefinition {
private int precision;
private double minStartValue;
private double maxStartValue;
private double minEndValue;
private double maxEndValue;
}

36
application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncrementTelemetryGenerator.java

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile;
import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.randomDouble;
public class IncrementTelemetryGenerator extends IncDecTelemetryGenerator<IncrementValueStrategyDefinition> {
public IncrementTelemetryGenerator(TelemetryProfile telemetryProfile) {
super(telemetryProfile);
}
@Override
public void addValue(long ts, ObjectNode values) {
double step = randomDouble(strategy.getMinIncrement(), strategy.getMaxIncrement());
double newValue = value + step;
value = Math.min(newValue, endValue);
put(values, value);
}
}

34
application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncrementValueStrategyDefinition.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class IncrementValueStrategyDefinition extends IncDecValueStrategyDefinition {
private double minIncrement;
private double maxIncrement;
@Override
public ValueStrategyDefinitionType getStrategyType() {
return ValueStrategyDefinitionType.INCREMENT;
}
}

94
application/src/main/java/org/thingsboard/server/service/solutions/data/values/NaturalTelemetryGenerator.java

@ -0,0 +1,94 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter;
import lombok.Setter;
import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile;
import java.math.BigDecimal;
import java.math.RoundingMode;
import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.getMultiplier;
import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.randomDouble;
public class NaturalTelemetryGenerator extends TelemetryGenerator {
private final NaturalValueStrategyDefinition strategy;
@Getter @Setter
private double value;
private boolean isIncrement;
private double lowValue;
private double highValue;
public NaturalTelemetryGenerator(TelemetryProfile telemetryProfile) {
super(telemetryProfile);
this.strategy = (NaturalValueStrategyDefinition) telemetryProfile.getValueStrategy();
this.value = getRandomStartValue();
this.lowValue = getRandomLowValue();
this.highValue = getRandomHighValue();
isIncrement = !strategy.isDecrementOnStart();
}
@Override
public void addValue(long ts, ObjectNode values) {
double multiplier = getMultiplier(ts, strategy.getHolidayMultiplier(), strategy.getWorkHoursMultiplier(), strategy.getNightHoursMultiplier());
if (isIncrement) {
double step = randomDouble(strategy.getMinIncrement(), strategy.getMaxIncrement());
value += step * multiplier;
if (value > highValue) {
value = highValue;
highValue = getRandomHighValue();
isIncrement = false;
}
} else {
double step = randomDouble(strategy.getMinDecrement(), strategy.getMaxDecrement());
value -= step * multiplier;
if (value < lowValue) {
value = lowValue;
lowValue = getRandomLowValue();
isIncrement = true;
}
}
put(values, value);
}
private void put(ObjectNode values, double value) {
if (strategy.getPrecision() == 0) {
values.put(key, (int) value);
} else {
values.put(key, BigDecimal.valueOf(value)
.setScale(strategy.getPrecision(), RoundingMode.HALF_UP)
.doubleValue());
}
}
public double getRandomStartValue() {
return randomDouble(strategy.getMinStartValue(), strategy.getMaxStartValue());
}
public double getRandomLowValue() {
return randomDouble(strategy.getMinLowValue(), strategy.getMaxLowValue());
}
public double getRandomHighValue() {
return randomDouble(strategy.getMinHighValue(), strategy.getMaxHighValue());
}
}

45
application/src/main/java/org/thingsboard/server/service/solutions/data/values/NaturalValueStrategyDefinition.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class NaturalValueStrategyDefinition implements ValueStrategyDefinition {
private int precision;
private double minStartValue;
private double maxStartValue;
private double minLowValue;
private double maxLowValue;
private double minHighValue;
private double maxHighValue;
private double minIncrement;
private double maxIncrement;
private double minDecrement;
private double maxDecrement;
private double holidayMultiplier;
private double workHoursMultiplier;
private double nightHoursMultiplier;
private boolean decrementOnStart;
@Override
public ValueStrategyDefinitionType getStrategyType() {
return ValueStrategyDefinitionType.NATURAL;
}
}

35
application/src/main/java/org/thingsboard/server/service/solutions/data/values/ScheduleValueStrategyDefinition.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ScheduleValueStrategyDefinition implements ValueStrategyDefinition {
private List<ValueStrategySchedule> schedule;
private String timeZone;
private ValueStrategyDefinition defaultDefinition;
@Override
public ValueStrategyDefinitionType getStrategyType() {
return ValueStrategyDefinitionType.SCHEDULE;
}
}

70
application/src/main/java/org/thingsboard/server/service/solutions/data/values/ScheduleValueStrategyGenerator.java

@ -0,0 +1,70 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TimeZone;
public class ScheduleValueStrategyGenerator extends TelemetryGenerator {
private TelemetryGenerator defaultGenerator;
private TimeZone timeZone;
private Map<ValueStrategySchedule, TelemetryGenerator> scheduleGenerators;
private TelemetryGenerator prevGenerator;
public ScheduleValueStrategyGenerator(TelemetryProfile tp) {
super(tp);
var def = (ScheduleValueStrategyDefinition) tp.getValueStrategy();
timeZone = TimeZone.getTimeZone(def.getTimeZone());
defaultGenerator = TelemetryGeneratorFactory.create(new TelemetryProfile(tp.getKey(), def.getDefaultDefinition()));
scheduleGenerators = new LinkedHashMap<>();
for (ValueStrategySchedule scheduleItem : def.getSchedule()) {
scheduleGenerators.put(scheduleItem, TelemetryGeneratorFactory.create(new TelemetryProfile(tp.getKey(), scheduleItem.getDefinition())));
}
}
@Override
public void addValue(long ts, ObjectNode values) {
int hour = GeneratorTools.getHour(timeZone, ts);
int minute = GeneratorTools.getMinute(timeZone, ts);
TelemetryGenerator generator = scheduleGenerators.entrySet().stream().filter(pair -> {
var schedule = pair.getKey();
if (hour == schedule.getStartHour() && hour == schedule.getEndHour()) {
return schedule.getStartMinute() <= minute && minute <= schedule.getEndMinute();
} else if (hour == schedule.getStartHour() && hour < schedule.getEndHour()) {
return schedule.getStartMinute() <= minute;
} else if (hour > schedule.getStartHour() && hour < schedule.getEndHour()) {
return true;
} else if (hour > schedule.getStartHour() && hour == schedule.getEndHour()) {
return minute <= schedule.getEndHour();
} else {
return false;
}
}).map(Map.Entry::getValue).findFirst().orElse(defaultGenerator);
if (prevGenerator != null && !prevGenerator.equals(generator)) {
generator.setValue(prevGenerator.getValue());
}
generator.addValue(ts, values);
prevGenerator = generator;
}
}

34
application/src/main/java/org/thingsboard/server/service/solutions/data/values/SequenceValueStrategyDefinition.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class SequenceValueStrategyDefinition implements ValueStrategyDefinition {
private boolean random;
private ObjectNode telemetry;
@Override
public ValueStrategyDefinitionType getStrategyType() {
return ValueStrategyDefinitionType.SEQUENCE;
}
}

48
application/src/main/java/org/thingsboard/server/service/solutions/data/values/SequenceValueStrategyGenerator.java

@ -0,0 +1,48 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile;
import java.util.Random;
public class SequenceValueStrategyGenerator extends TelemetryGenerator {
private final SequenceValueStrategyDefinition strategy;
private int max;
private int index;
public SequenceValueStrategyGenerator(TelemetryProfile telemetryProfile) {
super(telemetryProfile);
this.strategy = (SequenceValueStrategyDefinition) telemetryProfile.getValueStrategy();
max = strategy.getTelemetry().fields().next().getValue().size() - 1;
index = strategy.isRandom() ? new Random().nextInt(max + 1) : 0;
}
@Override
public void addValue(long ts, ObjectNode values) {
strategy.getTelemetry().fields().forEachRemaining(entry -> {
String key = entry.getKey();
values.set(key, entry.getValue().get(index));
});
index++;
if (index > max) {
index = 0;
}
}
}

41
application/src/main/java/org/thingsboard/server/service/solutions/data/values/TelemetryGenerator.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile;
@Data
public abstract class TelemetryGenerator {
protected final TelemetryProfile profile;
protected final String key;
public TelemetryGenerator(TelemetryProfile telemetryProfile) {
this.profile = telemetryProfile;
this.key = telemetryProfile.getKey();
}
public double getValue() {
throw new RuntimeException("Not supported");
}
public void setValue(double value) {
throw new RuntimeException("Not supported");
}
public abstract void addValue(long ts, ObjectNode values);
}

48
application/src/main/java/org/thingsboard/server/service/solutions/data/values/TelemetryGeneratorFactory.java

@ -0,0 +1,48 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile;
public class TelemetryGeneratorFactory {
public static TelemetryGenerator create(TelemetryProfile tp) {
switch (tp.getValueStrategy().getStrategyType()) {
case COUNTER:
return new CounterTelemetryGenerator(tp);
case NATURAL:
return new NaturalTelemetryGenerator(tp);
case EVENT:
return new EventTelemetryGenerator(tp);
case CONSTANT:
return new ConstantTelemetryGenerator(tp);
case SEQUENCE:
return new SequenceValueStrategyGenerator(tp);
case COMPOSITE:
return new CompositeValueStrategyGenerator(tp);
case SCHEDULE:
return new ScheduleValueStrategyGenerator(tp);
case INCREMENT:
return new IncrementTelemetryGenerator(tp);
case DECREMENT:
return new DecrementTelemetryGenerator(tp);
default:
throw new RuntimeException("Not supported!");
}
}
}

39
application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategyDefinition.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = CounterValueStrategyDefinition.class, name = "counter"),
@JsonSubTypes.Type(value = NaturalValueStrategyDefinition.class, name = "natural"),
@JsonSubTypes.Type(value = EventValueStrategyDefinition.class, name = "event"),
@JsonSubTypes.Type(value = SequenceValueStrategyDefinition.class, name = "sequence"),
@JsonSubTypes.Type(value = ConstantValueStrategyDefinition.class, name = "constant"),
@JsonSubTypes.Type(value = CompositeValueStrategyDefinition.class, name = "composite"),
@JsonSubTypes.Type(value = ScheduleValueStrategyDefinition.class, name = "schedule"),
@JsonSubTypes.Type(value = IncrementValueStrategyDefinition.class, name = "inc"),
@JsonSubTypes.Type(value = DecrementValueStrategyDefinition.class, name = "dec")})
public interface ValueStrategyDefinition {
ValueStrategyDefinitionType getStrategyType();
}

22
application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategyDefinitionType.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
public enum ValueStrategyDefinitionType {
COUNTER, NATURAL, EVENT, SEQUENCE, CONSTANT, COMPOSITE, SCHEDULE, INCREMENT, DECREMENT;
}

31
application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategySchedule.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.solutions.data.values;
import lombok.Data;
@Data
public class ValueStrategySchedule {
private int startHour;
private int startMinute;
private int endHour;
private int endMinute;
private ValueStrategyDefinition definition;
}

3
common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java

@ -114,6 +114,9 @@ public class DeviceProfile extends BaseData<DeviceProfileId> implements HasName,
this.description = deviceProfile.getDescription(); this.description = deviceProfile.getDescription();
this.image = deviceProfile.getImage(); this.image = deviceProfile.getImage();
this.isDefault = deviceProfile.isDefault(); this.isDefault = deviceProfile.isDefault();
this.type = deviceProfile.getType();
this.transportType = deviceProfile.getTransportType();
this.provisionType = deviceProfile.getProvisionType();
this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId(); this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId();
this.defaultDashboardId = deviceProfile.getDefaultDashboardId(); this.defaultDashboardId = deviceProfile.getDefaultDashboardId();
this.defaultQueueName = deviceProfile.getDefaultQueueName(); this.defaultQueueName = deviceProfile.getDefaultQueueName();

3
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java

@ -31,7 +31,8 @@ import java.io.Serializable;
@Type(name = "DASHBOARD", value = DashboardInstalledItemDescriptor.class), @Type(name = "DASHBOARD", value = DashboardInstalledItemDescriptor.class),
@Type(name = "CALCULATED_FIELD", value = CalculatedFieldInstalledItemDescriptor.class), @Type(name = "CALCULATED_FIELD", value = CalculatedFieldInstalledItemDescriptor.class),
@Type(name = "RULE_CHAIN", value = RuleChainInstalledItemDescriptor.class), @Type(name = "RULE_CHAIN", value = RuleChainInstalledItemDescriptor.class),
@Type(name = "DEVICE", value = DeviceInstalledItemDescriptor.class) @Type(name = "DEVICE", value = DeviceInstalledItemDescriptor.class),
@Type(name = "SOLUTION_TEMPLATE", value = SolutionTemplateInstalledItemDescriptor.class)
}) })
public interface IotHubInstalledItemDescriptor extends Serializable { public interface IotHubInstalledItemDescriptor extends Serializable {

34
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/SolutionTemplateInstalledItemDescriptor.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.iot_hub;
import lombok.Data;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.List;
@Data
public class SolutionTemplateInstalledItemDescriptor implements IotHubInstalledItemDescriptor {
private List<EntityId> createdEntityIds;
private DashboardId dashboardId;
private CustomerId publicId;
private boolean mainDashboardPublic;
private String details;
}

3
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -207,6 +207,7 @@ import { CheckConnectivityDialogComponent } from '@home/components/ai-model/chec
import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialog.component'; import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialog.component';
import { ResourcesDialogComponent } from "@home/components/resources/resources-dialog.component"; import { ResourcesDialogComponent } from "@home/components/resources/resources-dialog.component";
import { ResourcesLibraryComponent } from "@home/components/resources/resources-library.component"; import { ResourcesLibraryComponent } from "@home/components/resources/resources-library.component";
import { SolutionInstallDialogComponent } from '@home/components/solution/solution-install-dialog.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -362,6 +363,7 @@ import { ResourcesLibraryComponent } from "@home/components/resources/resources-
AIModelDialogComponent, AIModelDialogComponent,
ResourcesDialogComponent, ResourcesDialogComponent,
ResourcesLibraryComponent, ResourcesLibraryComponent,
SolutionInstallDialogComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -511,6 +513,7 @@ import { ResourcesLibraryComponent } from "@home/components/resources/resources-
AIModelDialogComponent, AIModelDialogComponent,
ResourcesDialogComponent, ResourcesDialogComponent,
ResourcesLibraryComponent, ResourcesLibraryComponent,
SolutionInstallDialogComponent,
], ],
providers: [ providers: [
WidgetComponentService, WidgetComponentService,

44
ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.html

@ -0,0 +1,44 @@
<!--
Copyright © 2016-2026 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.
-->
<div style="min-width:400px;" class="tb-dialog-content">
<mat-toolbar color="primary">
<h2>@if (instructions) {<mat-icon>info_outline</mat-icon>}{{ (instructions ? 'iot-hub.solution-instructions' : 'iot-hub.solution-install-title') | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<tb-markdown [data]="details" lineNumbers fallbackToPlainMarkdown></tb-markdown>
</div>
<div mat-dialog-actions class="flex flex-row items-center justify-end">
<button mat-button color="primary"
type="button"
(click)="close()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
@if (dashboardId) {
<button mat-raised-button color="primary"
(click)="gotoMainDashboard()">
{{ 'iot-hub.goto-main-dashboard' | translate }}
</button>
}
</div>
</div>

50
ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.scss

@ -0,0 +1,50 @@
/**
* Copyright © 2016-2026 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.
*/
:host {
h2 {
.mat-icon {
padding-right: 8px;
box-sizing: initial;
vertical-align: sub;
}
}
}
:host ::ng-deep {
.mat-mdc-dialog-content {
tb-markdown {
.tb-markdown-view {
.table-wrapper {
overflow-y: auto;
margin: 30px 32px;
border: 1px solid rgba(42, 125, 236, .2);
border-radius: 4px;
padding: 0;
width: calc(100% - 64px);
table {
width: 100%;
table-layout: auto;
white-space: nowrap;
margin: 0;
border: 0;
border-radius: 0;
}
}
}
}
}
}

59
ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.ts

@ -0,0 +1,59 @@
///
/// Copyright © 2016-2026 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { SolutionTemplateInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models';
export interface SolutionInstallDialogData {
descriptor: SolutionTemplateInstalledItemDescriptor;
instructions?: boolean;
}
@Component({
selector: 'tb-solution-install-dialog',
templateUrl: './solution-install-dialog.component.html',
styleUrls: ['./solution-install-dialog.component.scss'],
standalone: false
})
export class SolutionInstallDialogComponent {
details: string;
dashboardId: string | null;
instructions: boolean;
constructor(
@Inject(MAT_DIALOG_DATA) public data: SolutionInstallDialogData,
private dialogRef: MatDialogRef<SolutionInstallDialogComponent>,
private router: Router
) {
this.details = data.descriptor.details || '';
this.dashboardId = data.descriptor.dashboardId?.id || null;
this.instructions = !!data.instructions;
}
gotoMainDashboard(): void {
if (this.dashboardId) {
this.dialogRef.close();
this.router.navigateByUrl(`/dashboards/${this.dashboardId}`);
}
}
close(): void {
this.dialogRef.close();
}
}

5
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html

@ -42,6 +42,11 @@
(click)="onTypeTabChange(ItemType.DASHBOARD)"> (click)="onTypeTabChange(ItemType.DASHBOARD)">
{{ 'item.type-dashboard' | translate }}s {{ 'item.type-dashboard' | translate }}s
</a> </a>
<a mat-tab-link
[active]="activeType === ItemType.SOLUTION_TEMPLATE"
(click)="onTypeTabChange(ItemType.SOLUTION_TEMPLATE)">
{{ 'item.type-solution-template' | translate }}s
</a>
<a mat-tab-link <a mat-tab-link
[active]="activeType === ItemType.CALCULATED_FIELD" [active]="activeType === ItemType.CALCULATED_FIELD"
(click)="onTypeTabChange(ItemType.CALCULATED_FIELD)"> (click)="onTypeTabChange(ItemType.CALCULATED_FIELD)">

4
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts

@ -42,6 +42,7 @@ interface SortOption {
const TYPE_TABS: ItemType[] = [ const TYPE_TABS: ItemType[] = [
ItemType.WIDGET, ItemType.WIDGET,
ItemType.DASHBOARD, ItemType.DASHBOARD,
ItemType.SOLUTION_TEMPLATE,
ItemType.CALCULATED_FIELD, ItemType.CALCULATED_FIELD,
ItemType.RULE_CHAIN, ItemType.RULE_CHAIN,
ItemType.DEVICE ItemType.DEVICE
@ -296,6 +297,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
switch (this.activeType) { switch (this.activeType) {
case ItemType.WIDGET: return 'iot-hub.title-widgets'; case ItemType.WIDGET: return 'iot-hub.title-widgets';
case ItemType.DASHBOARD: return 'iot-hub.title-dashboards'; case ItemType.DASHBOARD: return 'iot-hub.title-dashboards';
case ItemType.SOLUTION_TEMPLATE: return 'iot-hub.title-solution-templates';
case ItemType.CALCULATED_FIELD: return 'iot-hub.title-calculated-fields'; case ItemType.CALCULATED_FIELD: return 'iot-hub.title-calculated-fields';
case ItemType.RULE_CHAIN: return 'iot-hub.title-rule-chains'; case ItemType.RULE_CHAIN: return 'iot-hub.title-rule-chains';
case ItemType.DEVICE: return 'iot-hub.title-devices'; case ItemType.DEVICE: return 'iot-hub.title-devices';
@ -306,6 +308,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
switch (this.activeType) { switch (this.activeType) {
case ItemType.WIDGET: return 'iot-hub.subtitle-widgets'; case ItemType.WIDGET: return 'iot-hub.subtitle-widgets';
case ItemType.DASHBOARD: return 'iot-hub.subtitle-dashboards'; case ItemType.DASHBOARD: return 'iot-hub.subtitle-dashboards';
case ItemType.SOLUTION_TEMPLATE: return 'iot-hub.subtitle-solution-templates';
case ItemType.CALCULATED_FIELD: return 'iot-hub.subtitle-calculated-fields'; case ItemType.CALCULATED_FIELD: return 'iot-hub.subtitle-calculated-fields';
case ItemType.RULE_CHAIN: return 'iot-hub.subtitle-rule-chains'; case ItemType.RULE_CHAIN: return 'iot-hub.subtitle-rule-chains';
case ItemType.DEVICE: return 'iot-hub.subtitle-devices'; case ItemType.DEVICE: return 'iot-hub.subtitle-devices';
@ -316,6 +319,7 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
switch (this.activeType) { switch (this.activeType) {
case ItemType.WIDGET: return 'iot-hub.search-widgets'; case ItemType.WIDGET: return 'iot-hub.search-widgets';
case ItemType.DASHBOARD: return 'iot-hub.search-dashboards'; case ItemType.DASHBOARD: return 'iot-hub.search-dashboards';
case ItemType.SOLUTION_TEMPLATE: return 'iot-hub.search-solution-templates';
case ItemType.CALCULATED_FIELD: return 'iot-hub.search-calculated-fields'; case ItemType.CALCULATED_FIELD: return 'iot-hub.search-calculated-fields';
case ItemType.RULE_CHAIN: return 'iot-hub.search-rule-chains'; case ItemType.RULE_CHAIN: return 'iot-hub.search-rule-chains';
case ItemType.DEVICE: return 'iot-hub.search-devices'; case ItemType.DEVICE: return 'iot-hub.search-devices';

22
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-install-dialog.component.ts

@ -15,16 +15,20 @@
/// ///
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models';
import { IotHubInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models'; import {
IotHubInstalledItemDescriptor,
SolutionTemplateInstalledItemDescriptor
} from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { getEntityDetailsPageURL } from '@core/utils'; import { getEntityDetailsPageURL } from '@core/utils';
import { SolutionInstallDialogComponent } from '@home/components/solution/solution-install-dialog.component';
export interface IotHubInstallDialogData { export interface IotHubInstallDialogData {
item: MpItemVersionView; item: MpItemVersionView;
@ -173,6 +177,7 @@ export class TbIotHubInstallDialogComponent {
constructor( constructor(
@Inject(MAT_DIALOG_DATA) public data: IotHubInstallDialogData, @Inject(MAT_DIALOG_DATA) public data: IotHubInstallDialogData,
private dialogRef: MatDialogRef<TbIotHubInstallDialogComponent>, private dialogRef: MatDialogRef<TbIotHubInstallDialogComponent>,
private dialog: MatDialog,
private router: Router, private router: Router,
private translate: TranslateService private translate: TranslateService
) { ) {
@ -199,8 +204,17 @@ export class TbIotHubInstallDialogComponent {
this.data.iotHubApiService.installItemVersion(versionId, { ignoreLoading: true }, data).subscribe({ this.data.iotHubApiService.installItemVersion(versionId, { ignoreLoading: true }, data).subscribe({
next: (result) => { next: (result) => {
if (result.success) { if (result.success) {
this.state = 'success'; if (result.descriptor?.type === 'SOLUTION_TEMPLATE') {
this.entityDetailsUrl = this.resolveEntityDetailsUrl(result.descriptor); this.dialogRef.close('installed');
this.dialog.open(SolutionInstallDialogComponent, {
disableClose: true,
panelClass: 'tb-solution-install-dialog-panel',
data: { descriptor: result.descriptor as SolutionTemplateInstalledItemDescriptor }
});
} else {
this.state = 'success';
this.entityDetailsUrl = this.resolveEntityDetailsUrl(result.descriptor);
}
} else { } else {
this.state = 'error'; this.state = 'error';
this.errorMessage = result.errorMessage || this.translate.instant('iot-hub.install-error', { name: this.item.name }); this.errorMessage = result.errorMessage || this.translate.instant('iot-hub.install-error', { name: this.item.name });

7
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.scss

@ -187,9 +187,9 @@
} }
.mat-column-itemType { .mat-column-itemType {
width: 150px; width: 170px;
min-width: 150px; min-width: 170px;
max-width: 150px; max-width: 170px;
} }
.mat-column-version { .mat-column-version {
@ -231,6 +231,7 @@
&.tb-type-calc-field { background: #fff3e0; color: #e65100; } &.tb-type-calc-field { background: #fff3e0; color: #e65100; }
&.tb-type-rule-chain { background: #f3e5f5; color: #7b1fa2; } &.tb-type-rule-chain { background: #f3e5f5; color: #7b1fa2; }
&.tb-type-device { background: #eceff1; color: #455a64; } &.tb-type-device { background: #eceff1; color: #455a64; }
&.tb-type-solution-template { background: #e0f7fa; color: #00695c; }
} }
.tb-iot-hub-up-to-date { .tb-iot-hub-up-to-date {

3
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts

@ -145,6 +145,7 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit {
case 'CALCULATED_FIELD': return 'tb-type-calc-field'; case 'CALCULATED_FIELD': return 'tb-type-calc-field';
case 'RULE_CHAIN': return 'tb-type-rule-chain'; case 'RULE_CHAIN': return 'tb-type-rule-chain';
case 'DEVICE': return 'tb-type-device'; case 'DEVICE': return 'tb-type-device';
case 'SOLUTION_TEMPLATE': return 'tb-type-solution-template';
default: return ''; default: return '';
} }
} }
@ -169,6 +170,7 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit {
case 'DASHBOARD': return descriptor.dashboardId?.id; case 'DASHBOARD': return descriptor.dashboardId?.id;
case 'CALCULATED_FIELD': return descriptor.entityId?.id; case 'CALCULATED_FIELD': return descriptor.entityId?.id;
case 'RULE_CHAIN': return descriptor.ruleChainId?.id; case 'RULE_CHAIN': return descriptor.ruleChainId?.id;
case 'SOLUTION_TEMPLATE': return descriptor.dashboardId?.id;
default: return null; default: return null;
} }
} }
@ -180,6 +182,7 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit {
case 'DASHBOARD': return EntityType.DASHBOARD; case 'DASHBOARD': return EntityType.DASHBOARD;
case 'CALCULATED_FIELD': return descriptor.entityId?.entityType as EntityType; case 'CALCULATED_FIELD': return descriptor.entityId?.entityType as EntityType;
case 'RULE_CHAIN': return EntityType.RULE_CHAIN; case 'RULE_CHAIN': return EntityType.RULE_CHAIN;
case 'SOLUTION_TEMPLATE': return EntityType.DASHBOARD;
default: return null; default: return null;
} }
} }

2
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.ts

@ -66,6 +66,7 @@ export class TbIotHubItemCardComponent {
case ItemType.CALCULATED_FIELD: return 'preview-calculated-field'; case ItemType.CALCULATED_FIELD: return 'preview-calculated-field';
case ItemType.RULE_CHAIN: return 'preview-rule-chain'; case ItemType.RULE_CHAIN: return 'preview-rule-chain';
case ItemType.DEVICE: return 'preview-device'; case ItemType.DEVICE: return 'preview-device';
case ItemType.SOLUTION_TEMPLATE: return 'preview-dashboard';
default: return ''; default: return '';
} }
} }
@ -74,6 +75,7 @@ export class TbIotHubItemCardComponent {
switch (this.item.type) { switch (this.item.type) {
case ItemType.WIDGET: return 'widgets'; case ItemType.WIDGET: return 'widgets';
case ItemType.DASHBOARD: return 'dashboard'; case ItemType.DASHBOARD: return 'dashboard';
case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions';
case ItemType.CALCULATED_FIELD: case ItemType.CALCULATED_FIELD:
return this.item.icon || cfTypeIcons.get(this.item.dataDescriptor?.cfType) || 'functions'; return this.item.icon || cfTypeIcons.get(this.item.dataDescriptor?.cfType) || 'functions';
case ItemType.RULE_CHAIN: case ItemType.RULE_CHAIN:

25
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html

@ -69,15 +69,15 @@
</div> </div>
} }
<!-- Dashboard: full-width image + description below --> <!-- Dashboard / Solution Template: full-width image + description below -->
@if (item.type === ItemType.DASHBOARD) { @if (item.type === ItemType.DASHBOARD || item.type === ItemType.SOLUTION_TEMPLATE) {
<div class="dlg-hero-full"> <div class="dlg-hero-full">
<div class="dlg-hero-full-image"> <div class="dlg-hero-full-image">
@if (getPreviewUrl(); as url) { @if (getPreviewUrl(); as url) {
<img [src]="url" alt=""> <img [src]="url" alt="">
} @else { } @else {
<div class="tb-iot-hub-preview-placeholder"> <div class="tb-iot-hub-preview-placeholder">
<tb-icon>dashboard</tb-icon> <tb-icon>{{ getTypeIcon() }}</tb-icon>
</div> </div>
} }
</div> </div>
@ -116,7 +116,7 @@
} }
<!-- CF/RC: description block --> <!-- CF/RC: description block -->
@if (item.description && item.type !== ItemType.WIDGET && item.type !== ItemType.DASHBOARD) { @if (item.description && item.type !== ItemType.WIDGET && item.type !== ItemType.DASHBOARD && item.type !== ItemType.SOLUTION_TEMPLATE) {
<div class="dlg-desc-block">{{ item.description }}</div> <div class="dlg-desc-block">{{ item.description }}</div>
} }
@ -175,10 +175,21 @@
<!-- Footer --> <!-- Footer -->
<div class="dlg-footer"> <div class="dlg-footer">
<div class="dlg-footer-actions"> <div class="dlg-footer-actions">
@if (isSameVersion()) { @if (isSameVersion() && item.type === ItemType.SOLUTION_TEMPLATE) {
<button class="dlg-install-btn" (click)="openEntityDetails()"> <button class="dlg-outlined-btn" (click)="openSolutionInstructions()">
{{ 'iot-hub.open-item-type-details' | translate:{ type: getTypeLabel() } }} {{ 'iot-hub.solution-instructions' | translate }}
</button> </button>
}
@if (isSameVersion()) {
@if (item.type === ItemType.SOLUTION_TEMPLATE) {
<button class="dlg-install-btn" (click)="openEntityDetails()">
{{ 'iot-hub.goto-main-dashboard' | translate }}
</button>
} @else {
<button class="dlg-install-btn" (click)="openEntityDetails()">
{{ 'iot-hub.open-item-type-details' | translate:{ type: getTypeLabel() } }}
</button>
}
} @else if (hasUpdate()) { } @else if (hasUpdate()) {
<button class="dlg-update-btn" (click)="updateItem()"> <button class="dlg-update-btn" (click)="updateItem()">
{{ 'iot-hub.update' | translate }} {{ 'iot-hub.update' | translate }}

18
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.scss

@ -393,6 +393,24 @@
width: 100%; width: 100%;
} }
.dlg-outlined-btn {
padding: 12px 24px;
border: 1px solid #305680;
border-radius: 8px;
background: transparent;
color: #305680;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
margin-right: auto;
&:hover {
background: #305680;
color: #fff;
}
}
.dlg-install-btn { .dlg-install-btn {
padding: 12px 24px; padding: 12px 24px;
border: none; border: none;

19
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.ts

@ -26,6 +26,8 @@ import { EntityType } from '@shared/models/entity-type.models';
import { getEntityDetailsPageURL } from '@core/utils'; import { getEntityDetailsPageURL } from '@core/utils';
import { TbIotHubInstallDialogComponent, IotHubInstallDialogData } from './iot-hub-install-dialog.component'; import { TbIotHubInstallDialogComponent, IotHubInstallDialogData } from './iot-hub-install-dialog.component';
import { TbIotHubUpdateDialogComponent, IotHubUpdateDialogData } from './iot-hub-update-dialog.component'; import { TbIotHubUpdateDialogComponent, IotHubUpdateDialogData } from './iot-hub-update-dialog.component';
import { SolutionInstallDialogComponent } from '@home/components/solution/solution-install-dialog.component';
import { SolutionTemplateInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models';
export interface IotHubItemDetailDialogData { export interface IotHubItemDetailDialogData {
item: MpItemVersionView; item: MpItemVersionView;
@ -84,6 +86,7 @@ export class TbIotHubItemDetailDialogComponent {
case ItemType.CALCULATED_FIELD: return 'type-calc-field'; case ItemType.CALCULATED_FIELD: return 'type-calc-field';
case ItemType.RULE_CHAIN: return 'type-rule-chain'; case ItemType.RULE_CHAIN: return 'type-rule-chain';
case ItemType.DEVICE: return 'type-device'; case ItemType.DEVICE: return 'type-device';
case ItemType.SOLUTION_TEMPLATE: return 'type-solution-template';
default: return ''; default: return '';
} }
} }
@ -97,6 +100,7 @@ export class TbIotHubItemDetailDialogComponent {
switch (this.item.type) { switch (this.item.type) {
case ItemType.WIDGET: return 'widgets'; case ItemType.WIDGET: return 'widgets';
case ItemType.DASHBOARD: return 'dashboard'; case ItemType.DASHBOARD: return 'dashboard';
case ItemType.SOLUTION_TEMPLATE: return 'integration_instructions';
case ItemType.CALCULATED_FIELD: return 'functions'; case ItemType.CALCULATED_FIELD: return 'functions';
case ItemType.RULE_CHAIN: return 'account_tree'; case ItemType.RULE_CHAIN: return 'account_tree';
case ItemType.DEVICE: return 'memory'; case ItemType.DEVICE: return 'memory';
@ -288,6 +292,7 @@ export class TbIotHubItemDetailDialogComponent {
entityType = descriptor.entityId?.entityType as EntityType; entityType = descriptor.entityId?.entityType as EntityType;
break; break;
case 'RULE_CHAIN': entityId = descriptor.ruleChainId?.id; entityType = EntityType.RULE_CHAIN; break; case 'RULE_CHAIN': entityId = descriptor.ruleChainId?.id; entityType = EntityType.RULE_CHAIN; break;
case 'SOLUTION_TEMPLATE': entityId = descriptor.dashboardId?.id; entityType = EntityType.DASHBOARD; break;
} }
if (entityType && entityId) { if (entityType && entityId) {
const url = getEntityDetailsPageURL(entityId, entityType); const url = getEntityDetailsPageURL(entityId, entityType);
@ -298,6 +303,20 @@ export class TbIotHubItemDetailDialogComponent {
} }
} }
openSolutionInstructions(): void {
const descriptor = this.installedItem?.descriptor;
if (descriptor?.type === 'SOLUTION_TEMPLATE') {
this.dialog.open(SolutionInstallDialogComponent, {
disableClose: true,
panelClass: 'tb-solution-install-dialog-panel',
data: {
descriptor: descriptor as SolutionTemplateInstalledItemDescriptor,
instructions: true
}
});
}
}
close(): void { close(): void {
this.dialogRef.close(); this.dialogRef.close();
} }

12
ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts

@ -41,12 +41,22 @@ export interface DeviceInstalledItemDescriptor {
type: 'DEVICE'; type: 'DEVICE';
} }
export interface SolutionTemplateInstalledItemDescriptor {
type: 'SOLUTION_TEMPLATE';
createdEntityIds: { entityType: string; id: string }[];
dashboardId: { id: string };
publicId: { id: string };
mainDashboardPublic: boolean;
details: string;
}
export type IotHubInstalledItemDescriptor = export type IotHubInstalledItemDescriptor =
| WidgetInstalledItemDescriptor | WidgetInstalledItemDescriptor
| DashboardInstalledItemDescriptor | DashboardInstalledItemDescriptor
| CalculatedFieldInstalledItemDescriptor | CalculatedFieldInstalledItemDescriptor
| RuleChainInstalledItemDescriptor | RuleChainInstalledItemDescriptor
| DeviceInstalledItemDescriptor; | DeviceInstalledItemDescriptor
| SolutionTemplateInstalledItemDescriptor;
export interface InstallItemVersionResult { export interface InstallItemVersionResult {
success: boolean; success: boolean;

26
ui-ngx/src/app/shared/models/iot-hub/iot-hub-item.models.ts

@ -17,6 +17,7 @@
export enum ItemType { export enum ItemType {
WIDGET = 'WIDGET', WIDGET = 'WIDGET',
DASHBOARD = 'DASHBOARD', DASHBOARD = 'DASHBOARD',
SOLUTION_TEMPLATE = 'SOLUTION_TEMPLATE',
CALCULATED_FIELD = 'CALCULATED_FIELD', CALCULATED_FIELD = 'CALCULATED_FIELD',
RULE_CHAIN = 'RULE_CHAIN', RULE_CHAIN = 'RULE_CHAIN',
DEVICE = 'DEVICE' DEVICE = 'DEVICE'
@ -26,6 +27,7 @@ export const itemTypeTranslations = new Map<ItemType, string>(
[ [
[ItemType.WIDGET, 'item.type-widget'], [ItemType.WIDGET, 'item.type-widget'],
[ItemType.DASHBOARD, 'item.type-dashboard'], [ItemType.DASHBOARD, 'item.type-dashboard'],
[ItemType.SOLUTION_TEMPLATE, 'item.type-solution-template'],
[ItemType.CALCULATED_FIELD, 'item.type-calculated-field'], [ItemType.CALCULATED_FIELD, 'item.type-calculated-field'],
[ItemType.RULE_CHAIN, 'item.type-rule-chain'], [ItemType.RULE_CHAIN, 'item.type-rule-chain'],
[ItemType.DEVICE, 'item.type-device'] [ItemType.DEVICE, 'item.type-device']
@ -114,6 +116,28 @@ export const ruleChainCategoryTranslations = new Map<RuleChainCategory, string>(
[RuleChainCategory.ANALYTICS, 'iot-hub.category.analytics'] [RuleChainCategory.ANALYTICS, 'iot-hub.category.analytics']
]); ]);
export enum SolutionTemplateCategory {
MONITORING = 'MONITORING',
ANALYTICS = 'ANALYTICS',
DEVICE_MANAGEMENT = 'DEVICE_MANAGEMENT',
ASSET_MANAGEMENT = 'ASSET_MANAGEMENT',
ENERGY = 'ENERGY',
SMART_CITY = 'SMART_CITY',
AGRICULTURE = 'AGRICULTURE',
INDUSTRIAL = 'INDUSTRIAL'
}
export const solutionTemplateCategoryTranslations = new Map<SolutionTemplateCategory, string>([
[SolutionTemplateCategory.MONITORING, 'iot-hub.category.monitoring'],
[SolutionTemplateCategory.ANALYTICS, 'iot-hub.category.analytics'],
[SolutionTemplateCategory.DEVICE_MANAGEMENT, 'iot-hub.category.device-management'],
[SolutionTemplateCategory.ASSET_MANAGEMENT, 'iot-hub.category.asset-management'],
[SolutionTemplateCategory.ENERGY, 'iot-hub.category.energy'],
[SolutionTemplateCategory.SMART_CITY, 'iot-hub.category.smart-city'],
[SolutionTemplateCategory.AGRICULTURE, 'iot-hub.category.agriculture'],
[SolutionTemplateCategory.INDUSTRIAL, 'iot-hub.category.industrial']
]);
export enum DeviceCategory { export enum DeviceCategory {
SENSORS = 'SENSORS', SENSORS = 'SENSORS',
GATEWAYS = 'GATEWAYS', GATEWAYS = 'GATEWAYS',
@ -162,6 +186,8 @@ export function getCategoriesForType(type: ItemType): Map<string, string> {
return widgetCategoryTranslations as Map<string, string>; return widgetCategoryTranslations as Map<string, string>;
case ItemType.DASHBOARD: case ItemType.DASHBOARD:
return dashboardCategoryTranslations as Map<string, string>; return dashboardCategoryTranslations as Map<string, string>;
case ItemType.SOLUTION_TEMPLATE:
return solutionTemplateCategoryTranslations as Map<string, string>;
case ItemType.CALCULATED_FIELD: case ItemType.CALCULATED_FIELD:
return calcFieldCategoryTranslations as Map<string, string>; return calcFieldCategoryTranslations as Map<string, string>;
case ItemType.RULE_CHAIN: case ItemType.RULE_CHAIN:

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

@ -3685,6 +3685,7 @@
"selected": "Selected", "selected": "Selected",
"type-widget": "Widget", "type-widget": "Widget",
"type-dashboard": "Dashboard", "type-dashboard": "Dashboard",
"type-solution-template": "Solution Template",
"type-calculated-field": "Calculated Field", "type-calculated-field": "Calculated Field",
"type-rule-chain": "Rule Chain", "type-rule-chain": "Rule Chain",
"type-device": "Device" "type-device": "Device"
@ -3698,13 +3699,16 @@
"title-calculated-fields": "Calculated Fields", "title-calculated-fields": "Calculated Fields",
"title-rule-chains": "Rule Chains", "title-rule-chains": "Rule Chains",
"title-devices": "Devices", "title-devices": "Devices",
"title-solution-templates": "Solution Templates",
"subtitle-widgets": "Browse community widgets for your dashboards", "subtitle-widgets": "Browse community widgets for your dashboards",
"subtitle-dashboards": "Browse ready-to-use dashboards", "subtitle-dashboards": "Browse ready-to-use dashboards",
"subtitle-solution-templates": "Browse complete IoT solution packages",
"subtitle-calculated-fields": "Browse calculated field templates", "subtitle-calculated-fields": "Browse calculated field templates",
"subtitle-rule-chains": "Browse rule chain templates", "subtitle-rule-chains": "Browse rule chain templates",
"subtitle-devices": "Browse device configurations", "subtitle-devices": "Browse device configurations",
"search-widgets": "Search widgets...", "search-widgets": "Search widgets...",
"search-dashboards": "Search dashboards...", "search-dashboards": "Search dashboards...",
"search-solution-templates": "Search solution templates...",
"search-calculated-fields": "Search calculated fields...", "search-calculated-fields": "Search calculated fields...",
"search-rule-chains": "Search rule chains...", "search-rule-chains": "Search rule chains...",
"search-devices": "Search devices...", "search-devices": "Search devices...",
@ -3744,6 +3748,9 @@
"install-success-title": "Installation Successful", "install-success-title": "Installation Successful",
"install-success-message": "'{{name}}' v{{version}} has been installed successfully.", "install-success-message": "'{{name}}' v{{version}} has been installed successfully.",
"install-error": "Failed to install {{name}}", "install-error": "Failed to install {{name}}",
"solution-install-title": "Solution template successfully installed",
"solution-instructions": "Solution instructions",
"goto-main-dashboard": "Go to main dashboard",
"install-error-title": "Installation Failed", "install-error-title": "Installation Failed",
"install-error-message": "Failed to install '{{name}}'.", "install-error-message": "Failed to install '{{name}}'.",
"check-for-updates": "Check for updates", "check-for-updates": "Check for updates",
@ -3787,6 +3794,9 @@
"category.alerting": "Alerting", "category.alerting": "Alerting",
"category.device-connectivity": "Device Connectivity", "category.device-connectivity": "Device Connectivity",
"category.integration": "Integration", "category.integration": "Integration",
"category.smart-city": "Smart City",
"category.agriculture": "Agriculture",
"category.industrial": "Industrial",
"category.sensors": "Sensors", "category.sensors": "Sensors",
"category.gateways": "Gateways", "category.gateways": "Gateways",
"category.controllers": "Controllers", "category.controllers": "Controllers",

Loading…
Cancel
Save