Browse Source

Merge feature/iot-hub into feature/lts-4.3-iot-hub

# Conflicts:
#	application/src/main/data/json/system/widget_bundles/home_page_widgets.json
#	application/src/main/data/upgrade/basic/schema_update.sql
#	application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java
#	dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
#	ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
#	ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts
#	ui-ngx/src/app/modules/home/components/home-components.module.ts
#	ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts
#	ui-ngx/src/app/shared/components/entity/entity-select.component.ts
pull/15547/head
Igor Kulikov 3 months ago
parent
commit
113ea3bf9b
  1. 3
      application/src/main/data/json/system/widget_bundles/home_page_widgets.json
  2. 39
      application/src/main/data/json/system/widget_types/iot_hub.json
  3. 16
      application/src/main/data/upgrade/basic/schema_update.sql
  4. 143
      application/src/main/java/org/thingsboard/server/controller/IotHubController.java
  5. 4
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  6. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java
  7. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java
  8. 607
      application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java
  9. 40
      application/src/main/java/org/thingsboard/server/service/iot_hub/InstallItemVersionResult.java
  10. 54
      application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java
  11. 33
      application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java
  12. 45
      application/src/main/java/org/thingsboard/server/service/iot_hub/UpdateItemVersionResult.java
  13. 1675
      application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java
  14. 34
      application/src/main/java/org/thingsboard/server/service/solutions/SolutionService.java
  15. 39
      application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedAlarmRuleInfo.java
  16. 39
      application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedCalculatedFieldInfo.java
  17. 49
      application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedEntityInfo.java
  18. 40
      application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedRuleChainInfo.java
  19. 28
      application/src/main/java/org/thingsboard/server/service/solutions/data/DashboardLinkInfo.java
  20. 30
      application/src/main/java/org/thingsboard/server/service/solutions/data/DeviceCredentialsInfo.java
  21. 28
      application/src/main/java/org/thingsboard/server/service/solutions/data/EdgeLinkInfo.java
  22. 209
      application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java
  23. 28
      application/src/main/java/org/thingsboard/server/service/solutions/data/UserCredentialsInfo.java
  24. 39
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/AssetDefinition.java
  25. 28
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/AssetProfileDefinition.java
  26. 48
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/BaseEntityDefinition.java
  27. 28
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CalculatedFieldDefinition.java
  28. 55
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CustomerDefinition.java
  29. 28
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CustomerEntityDefinition.java
  30. 38
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DashboardDefinition.java
  31. 26
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DashboardUserDetailsDefinition.java
  32. 41
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DeviceDefinition.java
  33. 28
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DeviceProfileDefinition.java
  34. 46
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EdgeDefinition.java
  35. 64
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EmulatorDefinition.java
  36. 26
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EntityDefinition.java
  37. 31
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EntitySearchKey.java
  38. 32
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/ReferenceableEntityDefinition.java
  39. 34
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/RelationDefinition.java
  40. 29
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/TelemetryProfile.java
  41. 29
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/TenantDefinition.java
  42. 40
      application/src/main/java/org/thingsboard/server/service/solutions/data/definition/UserDefinition.java
  43. 195
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/AbstractEmulatorLauncher.java
  44. 42
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/AssetEmulatorLauncher.java
  45. 42
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/BasicEmulator.java
  46. 25
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/CustomEmulator.java
  47. 98
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/DeviceEmulatorLauncher.java
  48. 24
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/Emulator.java
  49. 108
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/FieldIrrigationStateEmulator.java
  50. 24
      application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/SimpleEmulator.java
  51. 26
      application/src/main/java/org/thingsboard/server/service/solutions/data/names/RandomNameData.java
  52. 100
      application/src/main/java/org/thingsboard/server/service/solutions/data/names/RandomNameUtil.java
  53. 54
      application/src/main/java/org/thingsboard/server/service/solutions/data/solution/SolutionInstallResponse.java
  54. 46
      application/src/main/java/org/thingsboard/server/service/solutions/data/solution/TenantSolutionTemplateInstructions.java
  55. 34
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/CompositeValueStrategyDefinition.java
  56. 55
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/CompositeValueStrategyGenerator.java
  57. 51
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/ConstantTelemetryGenerator.java
  58. 31
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/ConstantValueStrategyDefinition.java
  59. 69
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/CounterTelemetryGenerator.java
  60. 42
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/CounterValueStrategyDefinition.java
  61. 37
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/DecrementTelemetryGenerator.java
  62. 34
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/DecrementValueStrategyDefinition.java
  63. 54
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/EventTelemetryGenerator.java
  64. 33
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/EventValueStrategyDefinition.java
  65. 96
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/GeneratorTools.java
  66. 64
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncDecTelemetryGenerator.java
  67. 32
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncDecValueStrategyDefinition.java
  68. 36
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncrementTelemetryGenerator.java
  69. 34
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncrementValueStrategyDefinition.java
  70. 94
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/NaturalTelemetryGenerator.java
  71. 45
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/NaturalValueStrategyDefinition.java
  72. 35
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/ScheduleValueStrategyDefinition.java
  73. 70
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/ScheduleValueStrategyGenerator.java
  74. 34
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/SequenceValueStrategyDefinition.java
  75. 48
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/SequenceValueStrategyGenerator.java
  76. 41
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/TelemetryGenerator.java
  77. 48
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/TelemetryGeneratorFactory.java
  78. 39
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategyDefinition.java
  79. 22
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategyDefinitionType.java
  80. 31
      application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategySchedule.java
  81. 2
      application/src/main/resources/thingsboard.yml
  82. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java
  83. 21
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DockerComposeParams.java
  84. 1
      common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
  85. 34
      common/data/src/main/java/org/thingsboard/server/common/data/id/IotHubInstalledItemId.java
  86. 28
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/CalculatedFieldInstalledItemDescriptor.java
  87. 26
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DashboardInstalledItemDescriptor.java
  88. 34
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.java
  89. 47
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItem.java
  90. 39
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java
  91. 26
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/RuleChainInstalledItemDescriptor.java
  92. 36
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/SolutionTemplateInstalledItemDescriptor.java
  93. 26
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/WidgetInstalledItemDescriptor.java
  94. 129
      common/data/src/test/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptorTest.java
  95. 26
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java
  96. 40
      dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java
  97. 46
      dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java
  98. 79
      dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java
  99. 12
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  100. 110
      dao/src/main/java/org/thingsboard/server/dao/model/sql/IotHubInstalledItemEntity.java

3
application/src/main/data/json/system/widget_bundles/home_page_widgets.json

@ -14,6 +14,7 @@
"home_page_widgets.documentation_links",
"home_page_widgets.dashboards",
"home_page_widgets.usage_info",
"api_usage"
"api_usage",
"home_page_widgets.iot_hub"
]
}

39
application/src/main/data/json/system/widget_types/iot_hub.json

File diff suppressed because one or more lines are too long

16
application/src/main/data/upgrade/basic/schema_update.sql

@ -80,3 +80,19 @@ DROP TABLE IF EXISTS calculated_field_link;
ANALYZE calculated_field;
-- REMOVAL OF CALCULATED FIELD LINKS PERSISTENCE END
-- IOT HUB INSTALLED ITEM START
CREATE TABLE IF NOT EXISTS iot_hub_installed_item (
id UUID NOT NULL PRIMARY KEY,
created_time BIGINT NOT NULL,
tenant_id UUID NOT NULL,
item_id UUID NOT NULL,
item_version_id UUID NOT NULL,
item_name VARCHAR NOT NULL,
item_type VARCHAR NOT NULL,
version VARCHAR NOT NULL,
descriptor JSONB NOT NULL
);
-- IOT HUB INSTALLED ITEM END

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

@ -0,0 +1,143 @@
/**
* 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.controller;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.iot_hub.DeviceInstalledItemDescriptor;
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.thingsboard.server.common.data.id.IotHubInstalledItemId;
import org.thingsboard.server.dao.device.DeviceConnectivityService;
import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import org.thingsboard.server.service.iot_hub.InstallItemVersionResult;
import org.thingsboard.server.service.iot_hub.UpdateItemVersionResult;
import org.thingsboard.server.service.iot_hub.IotHubService;
@Hidden
@RestController
@TbCoreComponent
@RequestMapping("/api/iot-hub")
@RequiredArgsConstructor
@Slf4j
public class IotHubController extends BaseController {
private final IotHubService iotHubService;
private final IotHubInstalledItemService iotHubInstalledItemService;
private final DeviceConnectivityService deviceConnectivityService;
private final SystemSecurityService systemSecurityService;
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/versions/{versionId}/install")
@ResponseBody
public InstallItemVersionResult installItemVersion(@PathVariable String versionId,
@RequestBody(required = false) JsonNode data,
HttpServletRequest request) throws ThingsboardException {
return iotHubService.installItemVersion(getCurrentUser(), versionId, data, request);
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/device/register")
@ResponseBody
public InstallItemVersionResult registerDeviceInstall(
@RequestParam String versionId,
@RequestBody JsonNode body) throws ThingsboardException {
DeviceInstalledItemDescriptor descriptor = JacksonUtil.treeToValue(body, DeviceInstalledItemDescriptor.class);
return iotHubService.registerDeviceInstall(getCurrentUser(), versionId, descriptor);
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/installedItems/{installedItemId}/update/{versionId}")
@ResponseBody
public UpdateItemVersionResult updateItemVersion(@PathVariable UUID installedItemId,
@PathVariable String versionId,
@RequestParam(required = false, defaultValue = "false") boolean force,
HttpServletRequest request) throws ThingsboardException {
return iotHubService.updateItemVersion(getCurrentUser(), new IotHubInstalledItemId(installedItemId), versionId, force, request);
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/installedItems")
@ResponseBody
public PageData<IotHubInstalledItem> getInstalledItems(@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder,
@RequestParam(required = false) List<String> itemTypes,
@RequestParam(required = false) UUID itemId) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return iotHubInstalledItemService.findByTenantId(getTenantId(), itemTypes, itemId, pageLink);
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/installedItems/count")
@ResponseBody
public long getInstalledItemsCount(@RequestParam(required = false) String itemType) throws ThingsboardException {
return iotHubInstalledItemService.countByTenantId(getTenantId(), itemType);
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/installedItems/itemIds")
@ResponseBody
public List<UUID> getInstalledItemIds() throws ThingsboardException {
return iotHubInstalledItemService.findInstalledItemIdsByTenantId(getTenantId());
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/installedItems/counts")
@ResponseBody
public Map<UUID, Long> getInstalledItemCounts(@RequestParam String itemType) throws ThingsboardException {
return iotHubInstalledItemService.findInstalledItemCounts(getTenantId(), itemType);
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@DeleteMapping("/installedItems/{installedItemId}")
@ResponseBody
public void deleteInstalledItem(@PathVariable UUID installedItemId) throws ThingsboardException {
iotHubService.deleteInstalledItem(getCurrentUser(), new IotHubInstalledItemId(installedItemId));
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/connectivity")
@ResponseBody
public JsonNode getConnectivitySettings(HttpServletRequest request) throws Exception {
String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
return deviceConnectivityService.getConnectivityInfo(baseUrl);
}
}

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

@ -76,6 +76,9 @@ public class SystemInfoController extends BaseController {
@Value("${debug.settings.default_duration:15}")
private int defaultDebugDurationMinutes;
@Value("${iot-hub.base-url:https://iot-hub.thingsboard.io}")
private String iotHubBaseUrl;
@Autowired(required = false)
private BuildProperties buildProperties;
@ -170,6 +173,7 @@ public class SystemInfoController extends BaseController {
systemParams.setIntermediateAggregationIntervalInSecForCF(tenantProfileConfiguration.getIntermediateAggregationIntervalInSecForCF());
systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId()));
}
systemParams.setIotHubBaseUrl(iotHubBaseUrl);
systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID))
.map(QrCodeSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage)
.orElse(false));

3
application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java

@ -32,6 +32,7 @@ import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal;
import org.thingsboard.script.api.tbel.TbelCfTsRollingArg;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
@ -99,7 +100,7 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp
@Override
@Transactional
public void delete(CalculatedField calculatedField, SecurityUser user) {
public void delete(CalculatedField calculatedField, User user) {
ActionType actionType = ActionType.DELETED;
TenantId tenantId = calculatedField.getTenantId();
CalculatedFieldId calculatedFieldId = calculatedField.getId();

3
application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.entitiy.cf;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -34,7 +35,7 @@ public interface TbCalculatedFieldService {
PageData<CalculatedField> findByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink);
void delete(CalculatedField calculatedField, SecurityUser user);
void delete(CalculatedField calculatedField, User user);
JsonNode executeTestScript(TenantId tenantId, JsonNode inputParams);

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

@ -0,0 +1,607 @@
/**
* 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.iot_hub;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.IotHubInstalledItemId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.iot_hub.CalculatedFieldInstalledItemDescriptor;
import org.thingsboard.server.common.data.iot_hub.DashboardInstalledItemDescriptor;
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.IotHubInstalledItemDescriptor;
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.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.NodeConnectionInfo;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService;
import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService;
import org.thingsboard.server.service.entitiy.device.TbDeviceService;
import org.thingsboard.server.service.entitiy.device.profile.TbDeviceProfileService;
import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService;
import org.thingsboard.server.service.rule.TbRuleChainService;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HexFormat;
import java.util.List;
import java.util.UUID;
@Service
@TbCoreComponent
@RequiredArgsConstructor
@Slf4j
public class DefaultIotHubService implements IotHubService {
private final IotHubRestClient iotHubRestClient;
private final TbWidgetTypeService tbWidgetTypeService;
private final TbDashboardService tbDashboardService;
private final TbCalculatedFieldService tbCalculatedFieldService;
private final RuleChainService ruleChainService;
private final TbRuleChainService tbRuleChainService;
private final TbDeviceProfileService tbDeviceProfileService;
private final IotHubInstalledItemService iotHubInstalledItemService;
private final WidgetTypeService widgetTypeService;
private final DashboardService dashboardService;
private final CalculatedFieldService calculatedFieldService;
private final DeviceProfileService deviceProfileService;
private final DeviceService deviceService;
private final TbDeviceService tbDeviceService;
private final SolutionService solutionService;
@Override
public InstallItemVersionResult installItemVersion(SecurityUser user, String versionId, JsonNode data, HttpServletRequest request) {
TenantId tenantId = user.getTenantId();
log.info("[{}] Installing IoT Hub item version: {}", tenantId, versionId);
try {
JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId);
String itemType = versionInfo.get("type").asText();
String itemName = versionInfo.get("name").asText();
UUID itemId = UUID.fromString(versionInfo.get("itemId").asText());
String version = versionInfo.get("version").asText();
log.debug("[{}] Fetched version info: {} (type: {})", tenantId, itemName, itemType);
byte[] fileData = iotHubRestClient.getVersionFileData(versionId);
log.debug("[{}] Fetched file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0);
IotHubInstalledItemDescriptor descriptor = switch (itemType) {
case "WIDGET" -> installWidget(user, tenantId, fileData);
case "DASHBOARD" -> installDashboard(user, tenantId, fileData);
case "CALCULATED_FIELD" -> installCalculatedField(user, tenantId, fileData, data);
case "RULE_CHAIN" -> installRuleChain(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);
};
IotHubInstalledItem installedItem = new IotHubInstalledItem();
installedItem.setTenantId(tenantId);
installedItem.setItemId(itemId);
installedItem.setItemVersionId(UUID.fromString(versionId));
installedItem.setItemName(itemName);
installedItem.setItemType(itemType);
installedItem.setVersion(version);
installedItem.setDescriptor(descriptor);
iotHubInstalledItemService.save(tenantId, installedItem);
iotHubRestClient.reportVersionInstalled(versionId);
log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType);
return InstallItemVersionResult.success(descriptor);
} catch (Exception e) {
log.error("[{}] Failed to install IoT Hub item version: {}", tenantId, versionId, e);
return InstallItemVersionResult.error(e.getMessage());
}
}
private WidgetInstalledItemDescriptor installWidget(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception {
WidgetTypeDetails widgetTypeDetails;
try {
widgetTypeDetails = JacksonUtil.fromString(new String(fileData), WidgetTypeDetails.class, true);
} catch (Exception e) {
throw new Exception("Failed to parse widget data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e);
}
widgetTypeDetails.setId(null);
widgetTypeDetails.setTenantId(tenantId);
WidgetTypeDetails saved = tbWidgetTypeService.save(widgetTypeDetails, false, user);
log.debug("[{}] Widget installed: {}", tenantId, saved.getName());
WidgetInstalledItemDescriptor descriptor = new WidgetInstalledItemDescriptor();
descriptor.setWidgetTypeId(saved.getId());
return descriptor;
}
private DashboardInstalledItemDescriptor installDashboard(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception {
Dashboard dashboard;
try {
dashboard = JacksonUtil.fromString(new String(fileData), Dashboard.class, true);
} catch (Exception e) {
throw new Exception("Failed to parse dashboard data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e);
}
dashboard.setId(null);
dashboard.setTenantId(tenantId);
Dashboard saved = tbDashboardService.save(dashboard, user);
log.debug("[{}] Dashboard installed: {}", tenantId, saved.getTitle());
DashboardInstalledItemDescriptor descriptor = new DashboardInstalledItemDescriptor();
descriptor.setDashboardId(saved.getId());
return descriptor;
}
private CalculatedFieldInstalledItemDescriptor installCalculatedField(SecurityUser user, TenantId tenantId, byte[] fileData, JsonNode data) throws Exception {
CalculatedField calculatedField;
try {
calculatedField = JacksonUtil.fromString(new String(fileData), CalculatedField.class, true);
} catch (Exception e) {
throw new Exception("Failed to parse calculated field data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e);
}
calculatedField.setId(null);
calculatedField.setTenantId(tenantId);
if (data != null && data.has("entityId")) {
EntityId entityId = JacksonUtil.treeToValue(data.get("entityId"), EntityId.class);
calculatedField.setEntityId(entityId);
}
CalculatedField saved = tbCalculatedFieldService.save(calculatedField, user);
log.debug("[{}] Calculated field installed: {}", tenantId, saved.getName());
CalculatedFieldInstalledItemDescriptor descriptor = new CalculatedFieldInstalledItemDescriptor();
descriptor.setCalculatedFieldId(saved.getId());
descriptor.setEntityId(saved.getEntityId());
return descriptor;
}
private RuleChainInstalledItemDescriptor installRuleChain(TenantId tenantId, byte[] fileData) throws Exception {
JsonNode json = JacksonUtil.toJsonNode(new String(fileData));
RuleChain ruleChain;
try {
ruleChain = JacksonUtil.fromString(json.get("ruleChain").toString(), RuleChain.class, true);
} catch (Exception e) {
throw new Exception("Failed to parse rule chain: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e);
}
RuleChainMetaData metadata;
try {
metadata = JacksonUtil.fromString(json.get("metadata").toString(), RuleChainMetaData.class, true);
} catch (Exception e) {
throw new Exception("Failed to parse rule chain metadata: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e);
}
ruleChain.setId(null);
ruleChain.setTenantId(tenantId);
RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain);
metadata.setRuleChainId(savedRuleChain.getId());
metadata.setVersion(savedRuleChain.getVersion());
ruleChainService.saveRuleChainMetaData(tenantId, metadata, tbRuleChainService::updateRuleNodeConfiguration);
log.debug("[{}] Rule chain installed: {}", tenantId, savedRuleChain.getName());
RuleChainInstalledItemDescriptor descriptor = new RuleChainInstalledItemDescriptor();
descriptor.setRuleChainId(savedRuleChain.getId());
return descriptor;
}
private DeviceInstalledItemDescriptor installDeviceProfile(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception {
DeviceProfile deviceProfile;
try {
deviceProfile = JacksonUtil.fromString(new String(fileData), DeviceProfile.class, true);
} catch (Exception e) {
throw new Exception("Failed to parse device profile data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e);
}
deviceProfile.setId(null);
deviceProfile.setTenantId(tenantId);
tbDeviceProfileService.save(deviceProfile, user);
log.debug("[{}] Device profile installed: {}", tenantId, deviceProfile.getName());
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.setTenantTelemetryKeys(response.getTenantTelemetryKeys());
descriptor.setTenantAttributeKeys(response.getTenantAttributeKeys());
descriptor.setDashboardId(response.getDashboardId());
descriptor.setPublicId(response.getPublicId());
descriptor.setMainDashboardPublic(response.isMainDashboardPublic());
descriptor.setDetails(response.getDetails());
return descriptor;
}
@Override
public UpdateItemVersionResult updateItemVersion(SecurityUser user, IotHubInstalledItemId installedItemId, String versionId, boolean force, HttpServletRequest request) {
TenantId tenantId = user.getTenantId();
log.info("[{}] Updating IoT Hub installed item {} to version: {}", tenantId, installedItemId, versionId);
try {
IotHubInstalledItem installedItem = iotHubInstalledItemService.findById(tenantId, installedItemId);
if (installedItem == null) {
throw new IllegalArgumentException("Installed item not found");
}
String itemType = installedItem.getItemType();
IotHubInstalledItemDescriptor descriptor = installedItem.getDescriptor();
// 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) {
String entityChecksum = calculateEntityChecksum(tenantId, installedItem);
boolean entityModified = installedChecksum != null && !installedChecksum.equals(entityChecksum);
log.info("[{}] Entity checksum: {}, modified: {}", tenantId, entityChecksum, entityModified);
if (entityModified) {
return UpdateItemVersionResult.entityModified();
}
}
}
JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId);
String itemName = versionInfo.get("name").asText();
String version = versionInfo.get("version").asText();
byte[] fileData = iotHubRestClient.getVersionFileData(versionId);
log.info("[{}] Fetched update file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0);
switch (itemType) {
case "WIDGET" -> updateWidget(user, tenantId, (WidgetInstalledItemDescriptor) descriptor, fileData);
case "DASHBOARD" -> updateDashboard(user, tenantId, (DashboardInstalledItemDescriptor) descriptor, fileData);
case "CALCULATED_FIELD" -> updateCalculatedField(user, tenantId, (CalculatedFieldInstalledItemDescriptor) descriptor, fileData);
case "RULE_CHAIN" -> updateRuleChain(tenantId, (RuleChainInstalledItemDescriptor) descriptor, fileData);
case "DEVICE" -> {
DeviceInstalledItemDescriptor dd = (DeviceInstalledItemDescriptor) descriptor;
deleteDevicePackageEntities(tenantId, dd.getCreatedEntityIds(), user);
dd.setCreatedEntityIds(null);
dd.setDashboardId(null);
}
case "SOLUTION_TEMPLATE" -> {
SolutionTemplateInstalledItemDescriptor stDescriptor = (SolutionTemplateInstalledItemDescriptor) descriptor;
solutionService.deleteSolution(tenantId, stDescriptor, 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);
}
installedItem.setItemVersionId(UUID.fromString(versionId));
installedItem.setItemName(itemName);
installedItem.setVersion(version);
iotHubInstalledItemService.save(tenantId, installedItem);
log.info("[{}] Successfully updated IoT Hub item {} to version: {}", tenantId, itemName, version);
return UpdateItemVersionResult.success(installedItem.getDescriptor());
} catch (Exception e) {
log.error("[{}] Failed to update IoT Hub installed item {} to version: {}", tenantId, installedItemId, versionId, e);
return UpdateItemVersionResult.error(e.getMessage());
}
}
private void updateWidget(SecurityUser user, TenantId tenantId, WidgetInstalledItemDescriptor descriptor, byte[] fileData) throws Exception {
WidgetTypeDetails newWidgetType;
try {
newWidgetType = JacksonUtil.fromString(new String(fileData), WidgetTypeDetails.class, true);
} catch (Exception e) {
throw new Exception("Failed to parse widget data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e);
}
WidgetTypeDetails existing = widgetTypeService.findWidgetTypeDetailsById(tenantId, descriptor.getWidgetTypeId());
if (existing == null) {
throw new Exception("Widget not found for update");
}
existing.setName(newWidgetType.getName());
existing.setDescriptor(newWidgetType.getDescriptor());
tbWidgetTypeService.save(existing, false, user);
}
private void updateDashboard(SecurityUser user, TenantId tenantId, DashboardInstalledItemDescriptor descriptor, byte[] fileData) throws Exception {
Dashboard newDashboard;
try {
newDashboard = JacksonUtil.fromString(new String(fileData), Dashboard.class, true);
} catch (Exception e) {
throw new Exception("Failed to parse dashboard data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e);
}
Dashboard existing = dashboardService.findDashboardById(tenantId, descriptor.getDashboardId());
if (existing == null) {
throw new Exception("Dashboard not found for update");
}
existing.setTitle(newDashboard.getTitle());
existing.setConfiguration(newDashboard.getConfiguration());
tbDashboardService.save(existing, user);
}
private void updateCalculatedField(SecurityUser user, TenantId tenantId, CalculatedFieldInstalledItemDescriptor descriptor, byte[] fileData) throws Exception {
CalculatedField newCf;
try {
newCf = JacksonUtil.fromString(new String(fileData), CalculatedField.class, true);
} catch (Exception e) {
throw new Exception("Failed to parse calculated field data: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e);
}
CalculatedField existing = calculatedFieldService.findById(tenantId, descriptor.getCalculatedFieldId());
if (existing == null) {
throw new Exception("Calculated field not found for update");
}
existing.setName(newCf.getName());
existing.setType(newCf.getType());
existing.setConfiguration(newCf.getConfiguration());
tbCalculatedFieldService.save(existing, user);
}
private void updateRuleChain(TenantId tenantId, RuleChainInstalledItemDescriptor descriptor, byte[] fileData) throws Exception {
JsonNode json = JacksonUtil.toJsonNode(new String(fileData));
RuleChainMetaData metadata;
try {
metadata = JacksonUtil.fromString(json.get("metadata").toString(), RuleChainMetaData.class, true);
} catch (Exception e) {
throw new Exception("Failed to parse rule chain metadata: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e);
}
RuleChain existing = ruleChainService.findRuleChainById(tenantId, descriptor.getRuleChainId());
if (existing == null) {
throw new Exception("Rule chain not found for update");
}
RuleChain newRuleChain;
try {
newRuleChain = JacksonUtil.fromString(json.get("ruleChain").toString(), RuleChain.class, true);
} catch (Exception e) {
throw new Exception("Failed to parse rule chain: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()), e);
}
existing.setName(newRuleChain.getName());
RuleChain savedRuleChain = ruleChainService.saveRuleChain(existing);
metadata.setRuleChainId(savedRuleChain.getId());
metadata.setVersion(savedRuleChain.getVersion());
ruleChainService.saveRuleChainMetaData(tenantId, metadata, tbRuleChainService::updateRuleNodeConfiguration);
}
private void updateDeviceProfile(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception {
// TODO: implement device profile update
}
private String calculateEntityChecksum(TenantId tenantId, IotHubInstalledItem installedItem) {
IotHubInstalledItemDescriptor descriptor = installedItem.getDescriptor();
if (descriptor instanceof WidgetInstalledItemDescriptor wd) {
return calculateWidgetChecksum(tenantId, wd);
} else if (descriptor instanceof DashboardInstalledItemDescriptor dd) {
return calculateDashboardChecksum(tenantId, dd);
} else if (descriptor instanceof CalculatedFieldInstalledItemDescriptor cd) {
return calculateCalculatedFieldChecksum(tenantId, cd);
} else if (descriptor instanceof RuleChainInstalledItemDescriptor rd) {
return calculateRuleChainChecksum(tenantId, rd);
}
return null;
}
private String calculateCalculatedFieldChecksum(TenantId tenantId, CalculatedFieldInstalledItemDescriptor descriptor) {
CalculatedField cf = calculatedFieldService.findById(tenantId, descriptor.getCalculatedFieldId());
if (cf == null) {
return null;
}
String content = (cf.getName() != null ? cf.getName() : "") +
(cf.getType() != null ? cf.getType().name() : "") +
(cf.getConfiguration() != null ? JacksonUtil.valueToTree(cf.getConfiguration()).toString() : "");
return sha256(content);
}
private String calculateDashboardChecksum(TenantId tenantId, DashboardInstalledItemDescriptor descriptor) {
Dashboard dashboard = dashboardService.findDashboardById(tenantId, descriptor.getDashboardId());
if (dashboard == null) {
return null;
}
String content = (dashboard.getTitle() != null ? dashboard.getTitle() : "") +
(dashboard.getConfiguration() != null ? dashboard.getConfiguration().toString() : "");
return sha256(content);
}
private String calculateWidgetChecksum(TenantId tenantId, WidgetInstalledItemDescriptor descriptor) {
WidgetTypeDetails widgetType = widgetTypeService.findWidgetTypeDetailsById(tenantId, descriptor.getWidgetTypeId());
if (widgetType == null) {
return null;
}
String content = (widgetType.getFqn() != null ? widgetType.getFqn() : "") +
(widgetType.getName() != null ? widgetType.getName() : "") +
(widgetType.getDescriptor() != null ? widgetType.getDescriptor().toString() : "");
return sha256(content);
}
private String calculateRuleChainChecksum(TenantId tenantId, RuleChainInstalledItemDescriptor descriptor) {
RuleChain ruleChain = ruleChainService.findRuleChainById(tenantId, descriptor.getRuleChainId());
if (ruleChain == null) {
return null;
}
RuleChainMetaData metadata = ruleChainService.loadRuleChainMetaData(tenantId, descriptor.getRuleChainId());
StringBuilder content = new StringBuilder();
content.append(ruleChain.getName() != null ? ruleChain.getName() : "");
content.append(ruleChain.getType() != null ? ruleChain.getType().name() : "");
if (metadata.getNodes() != null) {
for (RuleNode node : metadata.getNodes()) {
content.append(node.getType() != null ? node.getType() : "");
content.append(node.getName() != null ? node.getName() : "");
content.append(node.getConfiguration() != null ? node.getConfiguration().toString() : "");
}
}
if (metadata.getConnections() != null) {
for (NodeConnectionInfo conn : metadata.getConnections()) {
content.append(conn.getFromIndex());
content.append(conn.getToIndex());
content.append(conn.getType() != null ? conn.getType() : "");
}
}
return sha256(content.toString());
}
private static String sha256(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hash);
} catch (Exception e) {
throw new RuntimeException("Failed to calculate SHA-256", e);
}
}
@Override
public InstallItemVersionResult registerDeviceInstall(SecurityUser user, String versionId, DeviceInstalledItemDescriptor descriptor) {
TenantId tenantId = user.getTenantId();
log.info("[{}] Registering device package install for version: {}", tenantId, versionId);
try {
JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId);
String itemName = versionInfo.get("name").asText();
UUID itemId = UUID.fromString(versionInfo.get("itemId").asText());
String version = versionInfo.get("version").asText();
IotHubInstalledItem installedItem = new IotHubInstalledItem();
installedItem.setTenantId(tenantId);
installedItem.setItemId(itemId);
installedItem.setItemVersionId(UUID.fromString(versionId));
installedItem.setItemName(itemName);
installedItem.setItemType("DEVICE");
installedItem.setVersion(version);
installedItem.setDescriptor(descriptor);
iotHubInstalledItemService.save(tenantId, installedItem);
iotHubRestClient.reportVersionInstalled(versionId);
log.info("[{}] Registered device package install: {} (version {})", tenantId, itemName, version);
return InstallItemVersionResult.success(descriptor);
} catch (Exception e) {
log.error("[{}] Failed to register device package install: {}", tenantId, versionId, e);
return InstallItemVersionResult.error(e.getMessage());
}
}
private void deleteDevicePackageEntities(TenantId tenantId, List<EntityId> createdEntityIds, SecurityUser user) {
if (createdEntityIds == null || createdEntityIds.isEmpty()) {
return;
}
List<EntityId> reversed = new ArrayList<>(createdEntityIds);
Collections.reverse(reversed);
for (EntityId entityId : reversed) {
try {
deleteDevicePackageEntity(tenantId, entityId, user);
} catch (Exception e) {
log.error("[{}] Failed to delete device package entity: {}", tenantId, entityId, e);
}
}
}
private void deleteDevicePackageEntity(TenantId tenantId, EntityId entityId, SecurityUser user) {
switch (entityId.getEntityType()) {
case DEVICE -> {
var device = deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId()));
if (device != null) tbDeviceService.delete(device, user);
}
case DEVICE_PROFILE -> {
var profile = deviceProfileService.findDeviceProfileById(tenantId, new DeviceProfileId(entityId.getId()));
if (profile != null) tbDeviceProfileService.delete(profile, user);
}
case DASHBOARD -> {
var dashboard = dashboardService.findDashboardById(tenantId, new DashboardId(entityId.getId()));
if (dashboard != null) tbDashboardService.delete(dashboard, user);
}
case RULE_CHAIN -> {
var ruleChain = ruleChainService.findRuleChainById(tenantId, new RuleChainId(entityId.getId()));
if (ruleChain != null) tbRuleChainService.delete(ruleChain, user);
}
default -> log.warn("[{}] Unsupported entity type for device package delete: {}", tenantId, entityId.getEntityType());
}
}
@Override
public void deleteInstalledItem(SecurityUser user, IotHubInstalledItemId installedItemId) {
TenantId tenantId = user.getTenantId();
IotHubInstalledItem installedItem = iotHubInstalledItemService.findById(tenantId, installedItemId);
if (installedItem == null) {
throw new IllegalArgumentException("Installed item not found");
}
IotHubInstalledItemDescriptor descriptor = installedItem.getDescriptor();
if (descriptor instanceof WidgetInstalledItemDescriptor wd) {
WidgetTypeDetails widgetType = widgetTypeService.findWidgetTypeDetailsById(tenantId, wd.getWidgetTypeId());
if (widgetType != null) {
tbWidgetTypeService.delete(widgetType, user);
}
} else if (descriptor instanceof DashboardInstalledItemDescriptor dd) {
Dashboard dashboard = dashboardService.findDashboardById(tenantId, dd.getDashboardId());
if (dashboard != null) {
tbDashboardService.delete(dashboard, user);
}
} else if (descriptor instanceof CalculatedFieldInstalledItemDescriptor cd) {
CalculatedField calculatedField = calculatedFieldService.findById(tenantId, cd.getCalculatedFieldId());
if (calculatedField != null) {
tbCalculatedFieldService.delete(calculatedField, user);
}
} else if (descriptor instanceof RuleChainInstalledItemDescriptor rd) {
if (rd.getRuleChainId() != null) {
RuleChain ruleChain = ruleChainService.findRuleChainById(tenantId, rd.getRuleChainId());
if (ruleChain != null) {
tbRuleChainService.delete(ruleChain, user);
}
}
} else if (descriptor instanceof DeviceInstalledItemDescriptor dd) {
deleteDevicePackageEntities(tenantId, dd.getCreatedEntityIds(), user);
} else if (descriptor instanceof SolutionTemplateInstalledItemDescriptor st) {
try {
solutionService.deleteSolution(tenantId, st, user);
} catch (ThingsboardException e) {
log.error("[{}] Failed to delete solution for installed item {}", tenantId, installedItemId, e);
}
}
iotHubInstalledItemService.deleteById(tenantId, installedItemId);
log.info("[{}] Deleted installed IoT Hub item: {}", tenantId, installedItem.getItemName());
}
}

40
application/src/main/java/org/thingsboard/server/service/iot_hub/InstallItemVersionResult.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.iot_hub;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class InstallItemVersionResult {
private boolean success;
private String errorMessage;
private IotHubInstalledItemDescriptor descriptor;
public static InstallItemVersionResult success(IotHubInstalledItemDescriptor descriptor) {
return new InstallItemVersionResult(true, null, descriptor);
}
public static InstallItemVersionResult error(String errorMessage) {
return new InstallItemVersionResult(false, errorMessage, null);
}
}

54
application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.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.iot_hub;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.server.queue.util.TbCoreComponent;
@Component
@TbCoreComponent
@Slf4j
public class IotHubRestClient {
private final RestTemplate restTemplate = new RestTemplate();
@Value("${iot-hub.base-url:https://iot-hub.thingsboard.io}")
private String baseUrl;
public JsonNode getVersionInfo(String versionId) {
String url = baseUrl + "/api/versions/" + versionId;
log.debug("Fetching IoT Hub version info: {}", url);
return restTemplate.getForObject(url, JsonNode.class);
}
public byte[] getVersionFileData(String versionId) {
String url = baseUrl + "/api/versions/" + versionId + "/fileData";
log.debug("Fetching IoT Hub version file data: {}", url);
ResponseEntity<byte[]> response = restTemplate.getForEntity(url, byte[].class);
return response.getBody();
}
public void reportVersionInstalled(String versionId) {
String url = baseUrl + "/api/versions/" + versionId + "/install";
log.debug("Reporting IoT Hub version installed: {}", url);
restTemplate.postForObject(url, null, Void.class);
}
}

33
application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.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.iot_hub;
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.iot_hub.DeviceInstalledItemDescriptor;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface IotHubService {
InstallItemVersionResult installItemVersion(SecurityUser user, String versionId, JsonNode data, HttpServletRequest request);
UpdateItemVersionResult updateItemVersion(SecurityUser user, IotHubInstalledItemId installedItemId, String versionId, boolean force, HttpServletRequest request);
InstallItemVersionResult registerDeviceInstall(SecurityUser user, String versionId, DeviceInstalledItemDescriptor descriptor);
void deleteInstalledItem(SecurityUser user, IotHubInstalledItemId installedItemId);
}

45
application/src/main/java/org/thingsboard/server/service/iot_hub/UpdateItemVersionResult.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.iot_hub;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UpdateItemVersionResult {
private boolean success;
private boolean entityModified;
private String errorMessage;
private IotHubInstalledItemDescriptor descriptor;
public static UpdateItemVersionResult success(IotHubInstalledItemDescriptor descriptor) {
return new UpdateItemVersionResult(true, false, null, descriptor);
}
public static UpdateItemVersionResult entityModified() {
return new UpdateItemVersionResult(false, true, null, null);
}
public static UpdateItemVersionResult error(String errorMessage) {
return new UpdateItemVersionResult(false, false, errorMessage, null);
}
}

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

File diff suppressed because it is too large

34
application/src/main/java/org/thingsboard/server/service/solutions/SolutionService.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;
import jakarta.servlet.http.HttpServletRequest;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.iot_hub.SolutionTemplateInstalledItemDescriptor;
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, SolutionTemplateInstalledItemDescriptor descriptor, SecurityUser user) throws ThingsboardException;
}

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;
};
}
}

40
application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedRuleChainInfo.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;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.rule.RuleChainType;
import java.util.UUID;
@Data
@EqualsAndHashCode(callSuper = true)
public class CreatedRuleChainInfo extends CreatedEntityInfo {
private RuleChainType ruleChainType;
public CreatedRuleChainInfo(String name, RuleChainType ruleChainType, String owner) {
super(name, EntityType.RULE_CHAIN, owner);
this.ruleChainType = ruleChainType;
}
@Override
public String getEntityPageLink(UUID id) {
return ruleChainType == RuleChainType.EDGE ? "/edgeManagement/ruleChains/" + id : "/ruleChains/" + id;
}
}

28
ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.scss → application/src/main/java/org/thingsboard/server/service/solutions/data/DashboardLinkInfo.java

@ -13,24 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
min-width: 200px;
overflow: hidden;
background: #fff;
border-radius: 4px;
box-shadow:
0 7px 8px -4px rgba(0, 0, 0, .2),
0 13px 19px 2px rgba(0, 0, 0, .14),
0 5px 24px 4px rgba(0, 0, 0, .12);
package org.thingsboard.server.service.solutions.data;
.mat-content {
overflow: hidden;
background-color: #fff;
}
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;
.mat-padding {
padding: 16px;
}
}

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;
}

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

@ -0,0 +1,209 @@
/**
* 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.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 User 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 String solutionId;
private final long installTs;
private long oldestTelemetryTs;
private Map<String, EmulatorDefinition> deviceEmulators;
private Map<String, EmulatorDefinition> assetEmulators;
public SolutionInstallContext(TenantId tenantId, String solutionId, Path tempDir, User user, TenantSolutionTemplateInstructions solutionInstructions) {
this.tenantId = tenantId;
this.solutionId = solutionId;
this.tempDir = tempDir;
this.user = user;
this.solutionInstructions = solutionInstructions;
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 CreatedRuleChainInfo(ruleChain.getName(), ruleChain.getType(), "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;
}

46
application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EdgeDefinition.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.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();
private List<String> dashboardIds = 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)];
}
}

54
application/src/main/java/org/thingsboard/server/service/solutions/data/solution/SolutionInstallResponse.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.solution;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.Collections;
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;
@Schema(description = "What keys to delete during template uninstall")
private List<String> tenantTelemetryKeys;
@Schema(description = "What attributes to delete during template uninstall")
private List<String> tenantAttributeKeys;
public SolutionInstallResponse(TenantSolutionTemplateInstructions instructions, boolean success, List<EntityId> createdEntityIds) {
this(instructions, success, createdEntityIds, Collections.emptyList(), Collections.emptyList());
}
public SolutionInstallResponse(TenantSolutionTemplateInstructions instructions, boolean success, List<EntityId> createdEntityIds,
List<String> tenantTelemetryKeys, List<String> tenantAttributeKeys) {
super(instructions);
this.success = success;
this.createdEntityIds = createdEntityIds;
this.tenantTelemetryKeys = tenantTelemetryKeys;
this.tenantAttributeKeys = tenantAttributeKeys;
}
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;
}

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

@ -2174,3 +2174,5 @@ mqtt:
# The actual delay is randomized within a range defined by multiplying the base delay by a factor between (1 - jitter_factor) and (1 + jitter_factor).
# For example, a jitter_factor of 0.15 means the actual delay may vary by up to ±15% of the base delay.
jitter_factor: "${TB_MQTT_CLIENT_RETRANSMISSION_JITTER_FACTOR:0.15}"
iot-hub:
base-url: "${IOT_HUB_BASE_URL:https://iot-hub.thingsboard.io}" # IoT Hub base URL for fetching published items, resources, etc.

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

@ -25,7 +25,11 @@ public interface DeviceConnectivityService {
JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException;
JsonNode getConnectivityInfo(String baseUrl) throws URISyntaxException;
Resource getPemCertFile(String protocol);
Resource createGatewayDockerComposeFile(String baseUrl, Device device) throws URISyntaxException;
Resource createGatewayDockerComposeFile(String baseUrl, Device device, DockerComposeParams params) throws URISyntaxException;
}

21
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DockerComposeParams.java

@ -0,0 +1,21 @@
/**
* 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.dao.device;
public record DockerComposeParams(boolean includeVersion, String containerName, boolean includePortBindings,
boolean includeExtraHosts, boolean includeVolumesBind,
boolean includeVolumesDeclaration) {
}

1
common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java

@ -45,4 +45,5 @@ public class SystemParams {
long minAllowedAggregationIntervalInSecForCF;
long intermediateAggregationIntervalInSecForCF;
TrendzSettings trendzSettings;
String iotHubBaseUrl;
}

34
common/data/src/main/java/org/thingsboard/server/common/data/id/IotHubInstalledItemId.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.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.UUID;
public class IotHubInstalledItemId extends UUIDBased {
@JsonCreator
public IotHubInstalledItemId(@JsonProperty("id") UUID id) {
super(id);
}
public static IotHubInstalledItemId fromString(String id) {
return new IotHubInstalledItemId(UUID.fromString(id));
}
}

28
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/CalculatedFieldInstalledItemDescriptor.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.common.data.iot_hub;
import lombok.Data;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
@Data
public class CalculatedFieldInstalledItemDescriptor implements IotHubInstalledItemDescriptor {
private CalculatedFieldId calculatedFieldId;
private EntityId entityId;
}

26
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DashboardInstalledItemDescriptor.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.common.data.iot_hub;
import lombok.Data;
import org.thingsboard.server.common.data.id.DashboardId;
@Data
public class DashboardInstalledItemDescriptor implements IotHubInstalledItemDescriptor {
private DashboardId dashboardId;
}

34
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.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 com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.List;
import java.util.Map;
@Data
public class DeviceInstalledItemDescriptor implements IotHubInstalledItemDescriptor {
private List<EntityId> createdEntityIds;
private DashboardId dashboardId;
private String selectedInstallMethod;
private Map<String, JsonNode> installState;
}

47
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItem.java

@ -0,0 +1,47 @@
/**
* 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 lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.IotHubInstalledItemId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.UUID;
@Data
@EqualsAndHashCode(callSuper = true)
public class IotHubInstalledItem extends BaseData<IotHubInstalledItemId> implements HasTenantId {
private TenantId tenantId;
private UUID itemId;
private UUID itemVersionId;
private String itemName;
private String itemType;
private String version;
private IotHubInstalledItemDescriptor descriptor;
public IotHubInstalledItem() {
super();
}
public IotHubInstalledItem(IotHubInstalledItemId id) {
super(id);
}
}

39
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.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.common.data.iot_hub;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import java.io.Serializable;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "type")
@JsonSubTypes({
@Type(name = "WIDGET", value = WidgetInstalledItemDescriptor.class),
@Type(name = "DASHBOARD", value = DashboardInstalledItemDescriptor.class),
@Type(name = "CALCULATED_FIELD", value = CalculatedFieldInstalledItemDescriptor.class),
@Type(name = "RULE_CHAIN", value = RuleChainInstalledItemDescriptor.class),
@Type(name = "DEVICE", value = DeviceInstalledItemDescriptor.class),
@Type(name = "SOLUTION_TEMPLATE", value = SolutionTemplateInstalledItemDescriptor.class)
})
public interface IotHubInstalledItemDescriptor extends Serializable {
}

26
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/RuleChainInstalledItemDescriptor.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.common.data.iot_hub;
import lombok.Data;
import org.thingsboard.server.common.data.id.RuleChainId;
@Data
public class RuleChainInstalledItemDescriptor implements IotHubInstalledItemDescriptor {
private RuleChainId ruleChainId;
}

36
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/SolutionTemplateInstalledItemDescriptor.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.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 List<String> tenantTelemetryKeys;
private List<String> tenantAttributeKeys;
private DashboardId dashboardId;
private CustomerId publicId;
private boolean mainDashboardPublic;
private String details;
}

26
common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/WidgetInstalledItemDescriptor.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.common.data.iot_hub;
import lombok.Data;
import org.thingsboard.server.common.data.id.WidgetTypeId;
@Data
public class WidgetInstalledItemDescriptor implements IotHubInstalledItemDescriptor {
private WidgetTypeId widgetTypeId;
}

129
common/data/src/test/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptorTest.java

@ -0,0 +1,129 @@
/**
* 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 com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DashboardId;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
class DeviceInstalledItemDescriptorTest {
private static final ObjectMapper mapper = new ObjectMapper();
@Test
void deserializeFromFrontendPayload() throws Exception {
// Exact payload the frontend sends after the fix
String json = """
{
"type": "DEVICE",
"createdEntityIds": [
{"entityType": "DEVICE_PROFILE", "id": "3eb9c330-2e57-11f1-9334-a385172e8e7d"},
{"entityType": "DEVICE", "id": "8a90d5e0-2e5d-11f1-a802-c35a9af2ebde"},
{"entityType": "DASHBOARD", "id": "8bc229f0-2e5d-11f1-a802-c35a9af2ebde"}
],
"dashboardId": {"entityType": "DASHBOARD", "id": "8bc229f0-2e5d-11f1-a802-c35a9af2ebde"}
}
""";
// Deserialize the same way the controller does (JsonNode → treeToValue)
JsonNode node = mapper.readTree(json);
DeviceInstalledItemDescriptor descriptor = mapper.treeToValue(node, DeviceInstalledItemDescriptor.class);
assertThat(descriptor).isNotNull();
assertThat(descriptor.getCreatedEntityIds()).hasSize(3);
assertThat(descriptor.getCreatedEntityIds().get(0).getEntityType()).isEqualTo(EntityType.DEVICE_PROFILE);
assertThat(descriptor.getCreatedEntityIds().get(1).getEntityType()).isEqualTo(EntityType.DEVICE);
assertThat(descriptor.getCreatedEntityIds().get(2).getEntityType()).isEqualTo(EntityType.DASHBOARD);
assertThat(descriptor.getDashboardId()).isNotNull();
assertThat(descriptor.getDashboardId().getId()).isEqualTo(UUID.fromString("8bc229f0-2e5d-11f1-a802-c35a9af2ebde"));
}
@Test
void roundTripSerialization() throws Exception {
DeviceInstalledItemDescriptor original = new DeviceInstalledItemDescriptor();
DashboardId dashboardId = new DashboardId(UUID.randomUUID());
original.setDashboardId(dashboardId);
original.setCreatedEntityIds(java.util.List.of(dashboardId));
String json = mapper.writeValueAsString(original);
JsonNode node = mapper.readTree(json);
DeviceInstalledItemDescriptor deserialized = mapper.treeToValue(node, DeviceInstalledItemDescriptor.class);
assertThat(deserialized.getDashboardId().getId()).isEqualTo(original.getDashboardId().getId());
assertThat(deserialized.getCreatedEntityIds()).hasSize(1);
assertThat(deserialized.getCreatedEntityIds().get(0).getId()).isEqualTo(dashboardId.getId());
}
@Test
void deserializeWithInstallState() throws Exception {
String json = """
{
"type": "DEVICE",
"createdEntityIds": [
{"entityType": "DEVICE_PROFILE", "id": "3eb9c330-2e57-11f1-9334-a385172e8e7d"},
{"entityType": "DEVICE", "id": "8a90d5e0-2e5d-11f1-a802-c35a9af2ebde"},
{"entityType": "DASHBOARD", "id": "8bc229f0-2e5d-11f1-a802-c35a9af2ebde"}
],
"dashboardId": {"entityType": "DASHBOARD", "id": "8bc229f0-2e5d-11f1-a802-c35a9af2ebde"},
"selectedInstallMethod": "DIRECT_MQTT",
"installState": {
"Configuration": {
"formValues": {"deviceName": "ESP32 Dev Kit", "wifiSsid": "MyWiFi", "wifiPassword": "secret"}
},
"ESP32 Dev Kit": {
"entityOutput": {"id": "8a90d5e0-2e5d-11f1-a802-c35a9af2ebde", "name": "ESP32 Dev Kit", "token": "abc123", "url": "/entities/devices/8a90d5e0-2e5d-11f1-a802-c35a9af2ebde"}
},
"ESP32 Monitor": {
"entityOutput": {"id": "8bc229f0-2e5d-11f1-a802-c35a9af2ebde", "name": "ESP32 Monitor", "url": "/dashboards/8bc229f0-2e5d-11f1-a802-c35a9af2ebde"}
}
}
}
""";
JsonNode node = mapper.readTree(json);
DeviceInstalledItemDescriptor descriptor = mapper.treeToValue(node, DeviceInstalledItemDescriptor.class);
assertThat(descriptor).isNotNull();
assertThat(descriptor.getSelectedInstallMethod()).isEqualTo("DIRECT_MQTT");
assertThat(descriptor.getInstallState()).isNotNull();
assertThat(descriptor.getInstallState()).hasSize(3);
// Verify form values
JsonNode configState = descriptor.getInstallState().get("Configuration");
assertThat(configState).isNotNull();
assertThat(configState.get("formValues").get("deviceName").asText()).isEqualTo("ESP32 Dev Kit");
assertThat(configState.get("formValues").get("wifiSsid").asText()).isEqualTo("MyWiFi");
// Verify entity output
JsonNode deviceState = descriptor.getInstallState().get("ESP32 Dev Kit");
assertThat(deviceState).isNotNull();
assertThat(deviceState.get("entityOutput").get("token").asText()).isEqualTo("abc123");
// Verify round-trip
String serialized = mapper.writeValueAsString(descriptor);
JsonNode roundTripped = mapper.readTree(serialized);
DeviceInstalledItemDescriptor deserialized = mapper.treeToValue(roundTripped, DeviceInstalledItemDescriptor.class);
assertThat(deserialized.getSelectedInstallMethod()).isEqualTo("DIRECT_MQTT");
assertThat(deserialized.getInstallState()).hasSize(3);
assertThat(deserialized.getInstallState().get("Configuration").get("formValues").get("deviceName").asText()).isEqualTo("ESP32 Dev Kit");
}
}

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

@ -48,6 +48,7 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@ -155,11 +156,17 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
@Override
public Resource createGatewayDockerComposeFile(String baseUrl, Device device) throws URISyntaxException {
DockerComposeParams params = new DockerComposeParams(true, "tb-gateway", true, true, true, true);
return createGatewayDockerComposeFile(baseUrl, device, params);
}
@Override
public Resource createGatewayDockerComposeFile(String baseUrl, Device device, DockerComposeParams params) throws URISyntaxException {
String mqttType = isEnabled(MQTTS) ? MQTTS : MQTT;
DeviceConnectivityInfo properties = getConnectivity(mqttType);
DeviceCredentials creds = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId());
String host = getHost(baseUrl, properties, mqttType);
return DeviceConnectivityUtil.getGatewayDockerComposeFile(host, gatewayImageVersion, creds);
return DeviceConnectivityUtil.getGatewayDockerComposeFile(host, gatewayImageVersion, creds, params);
}
private DeviceConnectivityInfo getConnectivity(String protocol) {
@ -176,6 +183,23 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
return info != null && info.isEnabled();
}
@Override
public JsonNode getConnectivityInfo(String baseUrl) throws URISyntaxException {
String[] protocols = {HTTP, HTTPS, MQTT, MQTTS, COAP, COAPS};
Map<String, DeviceConnectivityInfo> result = new LinkedHashMap<>();
for (String protocol : protocols) {
DeviceConnectivityInfo info = getConnectivity(protocol);
if (info != null && info.isEnabled()) {
DeviceConnectivityInfo resolved = new DeviceConnectivityInfo();
resolved.setEnabled(true);
resolved.setHost(getHost(baseUrl, info, protocol));
resolved.setPort(getPort(info));
result.put(protocol, resolved);
}
}
return JacksonUtil.valueToTree(result);
}
private Resource getCert(String path) {
if (StringUtils.isBlank(path) || !ResourceUtils.resourceExists(this, path)) {
return null;

40
dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.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.dao.iot_hub;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public interface IotHubInstalledItemDao extends Dao<IotHubInstalledItem> {
PageData<IotHubInstalledItem> findByTenantId(TenantId tenantId, List<String> itemTypes, UUID itemId, PageLink pageLink);
List<UUID> findInstalledItemIdsByTenantId(TenantId tenantId);
long countByTenantId(TenantId tenantId, String itemType);
Map<UUID, Long> findInstalledItemCounts(TenantId tenantId, String itemType);
void deleteByTenantId(TenantId tenantId);
}

46
dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.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.dao.iot_hub;
import org.thingsboard.server.common.data.id.IotHubInstalledItemId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public interface IotHubInstalledItemService {
IotHubInstalledItem save(TenantId tenantId, IotHubInstalledItem item);
IotHubInstalledItem findById(TenantId tenantId, IotHubInstalledItemId id);
PageData<IotHubInstalledItem> findByTenantId(TenantId tenantId, List<String> itemTypes, UUID itemId, PageLink pageLink);
List<UUID> findInstalledItemIdsByTenantId(TenantId tenantId);
long countByTenantId(TenantId tenantId, String itemType);
Map<UUID, Long> findInstalledItemCounts(TenantId tenantId, String itemType);
void deleteById(TenantId tenantId, IotHubInstalledItemId id);
void deleteByTenantId(TenantId tenantId);
}

79
dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java

@ -0,0 +1,79 @@
/**
* 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.dao.iot_hub;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.IotHubInstalledItemId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Service
@RequiredArgsConstructor
@Slf4j
class IotHubInstalledItemServiceImpl implements IotHubInstalledItemService {
private final IotHubInstalledItemDao iotHubInstalledItemDao;
@Override
public IotHubInstalledItem save(TenantId tenantId, IotHubInstalledItem item) {
log.debug("[{}] Saving IoT Hub installed item: {}", tenantId, item);
return iotHubInstalledItemDao.save(tenantId, item);
}
@Override
public IotHubInstalledItem findById(TenantId tenantId, IotHubInstalledItemId id) {
return iotHubInstalledItemDao.findById(tenantId, id.getId());
}
@Override
public PageData<IotHubInstalledItem> findByTenantId(TenantId tenantId, List<String> itemTypes, UUID itemId, PageLink pageLink) {
return iotHubInstalledItemDao.findByTenantId(tenantId, itemTypes, itemId, pageLink);
}
@Override
public List<UUID> findInstalledItemIdsByTenantId(TenantId tenantId) {
return iotHubInstalledItemDao.findInstalledItemIdsByTenantId(tenantId);
}
@Override
public long countByTenantId(TenantId tenantId, String itemType) {
return iotHubInstalledItemDao.countByTenantId(tenantId, itemType);
}
@Override
public Map<UUID, Long> findInstalledItemCounts(TenantId tenantId, String itemType) {
return iotHubInstalledItemDao.findInstalledItemCounts(tenantId, itemType);
}
@Override
public void deleteById(TenantId tenantId, IotHubInstalledItemId id) {
iotHubInstalledItemDao.removeById(tenantId, id.getId());
}
@Override
public void deleteByTenantId(TenantId tenantId) {
iotHubInstalledItemDao.deleteByTenantId(tenantId);
}
}

12
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -763,6 +763,18 @@ public class ModelConstants {
public static final String API_KEY_ENABLED_COLUMN_NAME = "enabled";
public static final String API_KEY_DESCRIPTION_COLUMN_NAME = "description";
/**
* IoT Hub installed item constants.
*/
public static final String IOT_HUB_INSTALLED_ITEM_TABLE_NAME = "iot_hub_installed_item";
public static final String IOT_HUB_INSTALLED_ITEM_TENANT_ID_COLUMN = TENANT_ID_COLUMN;
public static final String IOT_HUB_INSTALLED_ITEM_ITEM_ID_COLUMN = "item_id";
public static final String IOT_HUB_INSTALLED_ITEM_ITEM_VERSION_ID_COLUMN = "item_version_id";
public static final String IOT_HUB_INSTALLED_ITEM_ITEM_NAME_COLUMN = "item_name";
public static final String IOT_HUB_INSTALLED_ITEM_ITEM_TYPE_COLUMN = "item_type";
public static final String IOT_HUB_INSTALLED_ITEM_VERSION_COLUMN = "version";
public static final String IOT_HUB_INSTALLED_ITEM_DESCRIPTOR_COLUMN = "descriptor";
protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN};
protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN), max(TS_COLUMN)};

110
dao/src/main/java/org/thingsboard/server/dao/model/sql/IotHubInstalledItemEntity.java

@ -0,0 +1,110 @@
/**
* 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.dao.model.sql;
import io.hypersistence.utils.hibernate.type.json.JsonBinaryType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.Type;
import org.hibernate.proxy.HibernateProxy;
import org.thingsboard.server.common.data.id.IotHubInstalledItemId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem;
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import java.util.Objects;
import java.util.UUID;
@Getter
@Setter
@ToString
@Entity
@Table(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_TABLE_NAME)
public class IotHubInstalledItemEntity extends BaseSqlEntity<IotHubInstalledItem> {
@Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_TENANT_ID_COLUMN, nullable = false, columnDefinition = "UUID")
private UUID tenantId;
@Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_ITEM_ID_COLUMN, nullable = false, columnDefinition = "UUID")
private UUID itemId;
@Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_ITEM_VERSION_ID_COLUMN, nullable = false, columnDefinition = "UUID")
private UUID itemVersionId;
@Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_ITEM_NAME_COLUMN, nullable = false)
private String itemName;
@Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_ITEM_TYPE_COLUMN, nullable = false)
private String itemType;
@Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_VERSION_COLUMN, nullable = false)
private String version;
@Type(JsonBinaryType.class)
@Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_DESCRIPTOR_COLUMN, nullable = false, columnDefinition = "JSONB")
private IotHubInstalledItemDescriptor descriptor;
public IotHubInstalledItemEntity() {
}
public IotHubInstalledItemEntity(IotHubInstalledItem item) {
super(item);
tenantId = getTenantUuid(item.getTenantId());
itemId = item.getItemId();
itemVersionId = item.getItemVersionId();
itemName = item.getItemName();
itemType = item.getItemType();
version = item.getVersion();
descriptor = item.getDescriptor();
}
@Override
public IotHubInstalledItem toData() {
var item = new IotHubInstalledItem(new IotHubInstalledItemId(id));
item.setCreatedTime(createdTime);
item.setTenantId(TenantId.fromUUID(tenantId));
item.setItemId(itemId);
item.setItemVersionId(itemVersionId);
item.setItemName(itemName);
item.setItemType(itemType);
item.setVersion(version);
item.setDescriptor(descriptor);
return item;
}
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
if (thisEffectiveClass != oEffectiveClass) return false;
IotHubInstalledItemEntity that = (IotHubInstalledItemEntity) o;
return getId() != null && Objects.equals(getId(), that.getId());
}
@Override
public final int hashCode() {
return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
}
}

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

Loading…
Cancel
Save