diff --git a/application/src/main/java/org/thingsboard/server/controller/IotHubController.java b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java index 86253a9195..a7de0596da 100644 --- a/application/src/main/java/org/thingsboard/server/controller/IotHubController.java +++ b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java @@ -31,6 +31,7 @@ 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.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; @@ -64,6 +65,15 @@ public class IotHubController extends BaseController { return iotHubService.installItemVersion(getCurrentUser(), versionId, data, request); } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/device/register") + @ResponseBody + public InstallItemVersionResult registerDeviceInstall( + @RequestParam String versionId, + @RequestBody DeviceInstalledItemDescriptor descriptor) throws ThingsboardException { + return iotHubService.registerDeviceInstall(getCurrentUser(), versionId, descriptor); + } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping("/installedItems/{installedItemId}/update/{versionId}") @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java index 5c2dfd937a..77cd7588cb 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java @@ -24,8 +24,12 @@ 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.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; @@ -45,12 +49,14 @@ 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; @@ -58,6 +64,8 @@ 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; @@ -80,6 +88,8 @@ public class DefaultIotHubService implements IotHubService { 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 @@ -283,7 +293,12 @@ public class DefaultIotHubService implements IotHubService { 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" -> updateDeviceProfile(user, tenantId, 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.getCreatedEntityIds(), user); @@ -474,6 +489,74 @@ public class DefaultIotHubService implements IotHubService { } } + @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 createdEntityIds, SecurityUser user) { + if (createdEntityIds == null || createdEntityIds.isEmpty()) { + return; + } + List 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(); @@ -505,8 +588,8 @@ public class DefaultIotHubService implements IotHubService { tbRuleChainService.delete(ruleChain, user); } } - } else if (descriptor instanceof DeviceInstalledItemDescriptor) { - // no entity to delete for now + } else if (descriptor instanceof DeviceInstalledItemDescriptor dd) { + deleteDevicePackageEntities(tenantId, dd.getCreatedEntityIds(), user); } else if (descriptor instanceof SolutionTemplateInstalledItemDescriptor st) { solutionService.deleteSolution(tenantId, st.getCreatedEntityIds(), user); } diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java index 0abc8f7756..e2f0ff0c39 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java @@ -18,6 +18,7 @@ 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 { @@ -26,5 +27,7 @@ public interface IotHubService { 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); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.java index b558058db5..6b345f9e59 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.java @@ -16,8 +16,15 @@ package org.thingsboard.server.common.data.iot_hub; import lombok.Data; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.List; @Data public class DeviceInstalledItemDescriptor implements IotHubInstalledItemDescriptor { + private List createdEntityIds; + private DashboardId dashboardId; + } diff --git a/ui-ngx/src/app/core/http/iot-hub-api.service.ts b/ui-ngx/src/app/core/http/iot-hub-api.service.ts index d6816b69d1..ef1fce640e 100644 --- a/ui-ngx/src/app/core/http/iot-hub-api.service.ts +++ b/ui-ngx/src/app/core/http/iot-hub-api.service.ts @@ -112,6 +112,18 @@ export class IotHubApiService { ); } + public registerDeviceInstall( + versionId: string, + descriptor: { createdEntityIds: { entityType: string; id: string }[]; dashboardId?: { id: string } }, + config?: IotHubRequestConfig + ): Observable { + return this.http.post( + `/api/iot-hub/device/register?versionId=${versionId}`, + descriptor, + { params: this.buildParams(config) } + ); + } + public installItemVersion(versionId: string, config?: IotHubRequestConfig, data?: any): Observable { return this.http.post( `/api/iot-hub/versions/${versionId}/install`, diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.ts index 203e5507e6..e0e6b60f90 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/device-install-dialog/device-install-dialog.component.ts @@ -371,15 +371,22 @@ export class TbDeviceInstallDialogComponent implements OnInit { } } - // All done — report install + // All done — register install with created entity IDs ws.progressDone = true; ws.completed = true; try { + const createdEntityIds = this.collectCreatedEntityIds(); + const dashboardId = this.findCreatedDashboardId(); await firstValueFrom( - this.data.iotHubApiService.reportVersionInstalled(this.data.item.id as string, { ignoreLoading: true }) + this.data.iotHubApiService.registerDeviceInstall( + this.data.item.id as string, + { createdEntityIds, dashboardId }, + { ignoreLoading: true } + ) ); } catch (_e) { - // Non-critical + // Non-critical — entities are created, tracking registration failed + console.error('Failed to register device install', _e); } // Auto-advance to next step after a short pause @@ -447,6 +454,46 @@ export class TbDeviceInstallDialogComponent implements OnInit { return match ? { id: match.id.id, name: match.name } : null; } + private collectCreatedEntityIds(): { entityType: string; id: string }[] { + const ids: { entityType: string; id: string }[] = []; + for (const ws of this.wizardSteps) { + if (ws.type === 'progress' && ws.entitySteps) { + for (const ep of ws.entitySteps) { + if (ep.status === 'success' && ep.entityOutput) { + const entityType = this.stepTypeToEntityType(ep.step.type); + if (entityType) { + ids.push({ entityType, id: ep.entityOutput.id }); + } + } + } + } + } + return ids; + } + + private findCreatedDashboardId(): { id: string } | undefined { + for (const ws of this.wizardSteps) { + if (ws.type === 'progress' && ws.entitySteps) { + for (const ep of ws.entitySteps) { + if (ep.step.type === InstallStepType.DASHBOARD && ep.status === 'success' && ep.entityOutput) { + return { id: ep.entityOutput.id }; + } + } + } + } + return undefined; + } + + private stepTypeToEntityType(stepType: InstallStepType): string | null { + switch (stepType) { + case InstallStepType.DEVICE_PROFILE: return 'DEVICE_PROFILE'; + case InstallStepType.DEVICE: return 'DEVICE'; + case InstallStepType.DASHBOARD: return 'DASHBOARD'; + case InstallStepType.RULE_CHAIN: return 'RULE_CHAIN'; + default: return null; + } + } + private delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts index a8401a2313..e8a875acea 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-installed-items.component.ts @@ -170,6 +170,7 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { case 'DASHBOARD': return descriptor.dashboardId?.id; case 'CALCULATED_FIELD': return descriptor.entityId?.id; case 'RULE_CHAIN': return descriptor.ruleChainId?.id; + case 'DEVICE': return descriptor.dashboardId?.id ?? null; case 'SOLUTION_TEMPLATE': return descriptor.dashboardId?.id; default: return null; } @@ -182,6 +183,7 @@ export class TbIotHubInstalledItemsComponent implements OnInit, AfterViewInit { case 'DASHBOARD': return EntityType.DASHBOARD; case 'CALCULATED_FIELD': return descriptor.entityId?.entityType as EntityType; case 'RULE_CHAIN': return EntityType.RULE_CHAIN; + case 'DEVICE': return descriptor.dashboardId ? EntityType.DASHBOARD : null; case 'SOLUTION_TEMPLATE': return EntityType.DASHBOARD; default: return null; } diff --git a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts index fe864cbb06..c2dff3af75 100644 --- a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts +++ b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-installed-item.models.ts @@ -39,6 +39,8 @@ export interface RuleChainInstalledItemDescriptor { export interface DeviceInstalledItemDescriptor { type: 'DEVICE'; + createdEntityIds?: { entityType: string; id: string }[]; + dashboardId?: { id: string }; } export interface SolutionTemplateInstalledItemDescriptor {