diff --git a/application/pom.xml b/application/pom.xml index e7cedeb540..303f206601 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -145,6 +145,10 @@ ${project.version} runtime + + org.springframework.integration + spring-integration-redis + org.springframework.boot spring-boot-starter-security diff --git a/application/src/main/data/upgrade/3.2.2/schema_update.sql b/application/src/main/data/upgrade/3.2.2/schema_update.sql index af3fd418a8..4fec49130a 100644 --- a/application/src/main/data/upgrade/3.2.2/schema_update.sql +++ b/application/src/main/data/upgrade/3.2.2/schema_update.sql @@ -78,14 +78,24 @@ CREATE TABLE IF NOT EXISTS firmware ( CONSTRAINT firmware_tenant_title_version_unq_key UNIQUE (tenant_id, title, version) ); +ALTER TABLE dashboard + ADD COLUMN IF NOT EXISTS image varchar(1000000); + ALTER TABLE device_profile + ADD COLUMN IF NOT EXISTS image varchar(1000000), ADD COLUMN IF NOT EXISTS firmware_id uuid, - ADD COLUMN IF NOT EXISTS software_id uuid; + ADD COLUMN IF NOT EXISTS software_id uuid, + ADD COLUMN IF NOT EXISTS default_dashboard_id uuid; ALTER TABLE device ADD COLUMN IF NOT EXISTS firmware_id uuid, ADD COLUMN IF NOT EXISTS software_id uuid; +ALTER TABLE alarm + ADD COLUMN IF NOT EXISTS customer_id uuid; + +DELETE FROM relation WHERE from_type = 'TENANT' AND relation_type_group = 'RULE_CHAIN'; + DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_firmware_device_profile') THEN @@ -100,6 +110,12 @@ DO $$ FOREIGN KEY (firmware_id) REFERENCES firmware(id); END IF; + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_default_dashboard_device_profile') THEN + ALTER TABLE device_profile + ADD CONSTRAINT fk_default_dashboard_device_profile + FOREIGN KEY (default_dashboard_id) REFERENCES dashboard(id); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_firmware_device') THEN ALTER TABLE device ADD CONSTRAINT fk_firmware_device @@ -114,3 +130,7 @@ DO $$ END; $$; + +ALTER TABLE api_usage_state + ADD COLUMN IF NOT EXISTS alarm_exec VARCHAR(32); +UPDATE api_usage_state SET alarm_exec = 'ENABLED' WHERE alarm_exec IS NULL; diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index 9453bd46b7..03883fc167 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -196,6 +196,40 @@ public class AlarmController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarms", method = RequestMethod.GET) + @ResponseBody + public PageData getAllAlarms( + @RequestParam(required = false) String searchStatus, + @RequestParam(required = false) String status, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime, + @RequestParam(required = false) Boolean fetchOriginator + ) throws ThingsboardException { + AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus); + AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status); + if (alarmSearchStatus != null && alarmStatus != null) { + throw new ThingsboardException("Invalid alarms search query: Both parameters 'searchStatus' " + + "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + + try { + if (getCurrentUser().isCustomerUser()) { + return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); + } else { + return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); + } + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}", method = RequestMethod.GET) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java b/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java index f22fd1c6e7..1c392913f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java @@ -106,6 +106,8 @@ public abstract class BaseApiUsageState { return apiUsageState.getEmailExecState(); case SMS: return apiUsageState.getSmsExecState(); + case ALARM: + return apiUsageState.getAlarmExecState(); default: return ApiUsageStateValue.ENABLED; } @@ -132,6 +134,9 @@ public abstract class BaseApiUsageState { case SMS: apiUsageState.setSmsExecState(value); break; + case ALARM: + apiUsageState.setAlarmExecState(value); + break; } return !currentValue.equals(value); } diff --git a/application/src/main/java/org/thingsboard/server/service/firmware/DefaultFirmwareStateService.java b/application/src/main/java/org/thingsboard/server/service/firmware/DefaultFirmwareStateService.java index ca369b3698..ed9f8a9eaf 100644 --- a/application/src/main/java/org/thingsboard/server/service/firmware/DefaultFirmwareStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/firmware/DefaultFirmwareStateService.java @@ -24,8 +24,9 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.FirmwareInfo; -import org.thingsboard.server.common.data.firmware.FirmwareUtil; import org.thingsboard.server.common.data.firmware.FirmwareType; +import org.thingsboard.server.common.data.firmware.FirmwareUpdateStatus; +import org.thingsboard.server.common.data.firmware.FirmwareUtil; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.FirmwareId; import org.thingsboard.server.common.data.id.TenantId; @@ -66,11 +67,11 @@ import static org.thingsboard.server.common.data.firmware.FirmwareKey.STATE; import static org.thingsboard.server.common.data.firmware.FirmwareKey.TITLE; import static org.thingsboard.server.common.data.firmware.FirmwareKey.TS; import static org.thingsboard.server.common.data.firmware.FirmwareKey.VERSION; +import static org.thingsboard.server.common.data.firmware.FirmwareType.FIRMWARE; +import static org.thingsboard.server.common.data.firmware.FirmwareType.SOFTWARE; import static org.thingsboard.server.common.data.firmware.FirmwareUtil.getAttributeKey; import static org.thingsboard.server.common.data.firmware.FirmwareUtil.getTargetTelemetryKey; import static org.thingsboard.server.common.data.firmware.FirmwareUtil.getTelemetryKey; -import static org.thingsboard.server.common.data.firmware.FirmwareType.FIRMWARE; -import static org.thingsboard.server.common.data.firmware.FirmwareType.SOFTWARE; @Slf4j @Service diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index 3f93aef41a..b91c46417c 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -27,6 +27,9 @@ import org.thingsboard.rule.engine.profile.TbDeviceProfileNode; import org.thingsboard.rule.engine.profile.TbDeviceProfileNodeConfiguration; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; @@ -34,9 +37,13 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.dao.alarm.AlarmDao; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; @@ -72,6 +79,15 @@ public class DefaultDataUpdateService implements DataUpdateService { @Autowired private TimeseriesService tsService; + @Autowired + private AlarmService alarmService; + + @Autowired + private EntityService entityService; + + @Autowired + private AlarmDao alarmDao; + @Override public void updateData(String fromVersion) throws Exception { switch (fromVersion) { @@ -90,14 +106,25 @@ public class DefaultDataUpdateService implements DataUpdateService { case "3.2.2": log.info("Updating data from version 3.2.2 to 3.3.0 ..."); tenantsDefaultEdgeRuleChainUpdater.updateEntities(null); + tenantsAlarmsCustomerUpdater.updateEntities(null); break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); } } - private PaginatedUpdater tenantsDefaultRuleChainUpdater = - new PaginatedUpdater() { + private final PaginatedUpdater tenantsDefaultRuleChainUpdater = + new PaginatedUpdater<>() { + + @Override + protected String getName() { + return "Tenants default rule chain updater"; + } + + @Override + protected boolean forceReportTotal() { + return true; + } @Override protected PageData findEntities(String region, PageLink pageLink) { @@ -117,8 +144,18 @@ public class DefaultDataUpdateService implements DataUpdateService { } }; - private PaginatedUpdater tenantsDefaultEdgeRuleChainUpdater = - new PaginatedUpdater() { + private final PaginatedUpdater tenantsDefaultEdgeRuleChainUpdater = + new PaginatedUpdater<>() { + + @Override + protected String getName() { + return "Tenants default edge rule chain updater"; + } + + @Override + protected boolean forceReportTotal() { + return true; + } @Override protected PageData findEntities(String region, PageLink pageLink) { @@ -138,8 +175,18 @@ public class DefaultDataUpdateService implements DataUpdateService { } }; - private PaginatedUpdater tenantsRootRuleChainUpdater = - new PaginatedUpdater() { + private final PaginatedUpdater tenantsRootRuleChainUpdater = + new PaginatedUpdater<>() { + + @Override + protected String getName() { + return "Tenants root rule chain updater"; + } + + @Override + protected boolean forceReportTotal() { + return true; + } @Override protected PageData findEntities(String region, PageLink pageLink) { @@ -192,8 +239,18 @@ public class DefaultDataUpdateService implements DataUpdateService { } }; - private PaginatedUpdater tenantsEntityViewsUpdater = - new PaginatedUpdater() { + private final PaginatedUpdater tenantsEntityViewsUpdater = + new PaginatedUpdater<>() { + + @Override + protected String getName() { + return "Tenants entity views updater"; + } + + @Override + protected boolean forceReportTotal() { + return true; + } @Override protected PageData findEntities(String region, PageLink pageLink) { @@ -261,4 +318,48 @@ public class DefaultDataUpdateService implements DataUpdateService { }, MoreExecutors.directExecutor()); } + private final PaginatedUpdater tenantsAlarmsCustomerUpdater = + new PaginatedUpdater<>() { + + @Override + protected String getName() { + return "Tenants alarms customer updater"; + } + + @Override + protected boolean forceReportTotal() { + return true; + } + + @Override + protected PageData findEntities(String region, PageLink pageLink) { + return tenantService.findTenants(pageLink); + } + + @Override + protected void updateEntity(Tenant tenant) { + updateTenantAlarmsCustomer(tenant.getId()); + } + }; + + private void updateTenantAlarmsCustomer(TenantId tenantId) { + AlarmQuery alarmQuery = new AlarmQuery(null, new TimePageLink(100), null, null, false); + PageData alarms = alarmDao.findAlarms(tenantId, alarmQuery); + boolean hasNext = true; + while (hasNext) { + for (Alarm alarm : alarms.getData()) { + if (alarm.getCustomerId() == null && alarm.getOriginator() != null) { + alarm.setCustomerId(entityService.fetchEntityCustomerId(tenantId, alarm.getOriginator())); + alarmDao.save(tenantId, alarm); + } + } + if (alarms.hasNext()) { + alarmQuery.setPageLink(alarmQuery.getPageLink().nextPageLink()); + alarms = alarmDao.findAlarms(tenantId, alarmQuery); + } else { + hasNext = false; + } + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java b/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java index e766bb0daa..4bfe744863 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java @@ -15,16 +15,20 @@ */ package org.thingsboard.server.service.install.update; +import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +@Slf4j public abstract class PaginatedUpdater> { private static final int DEFAULT_LIMIT = 100; + private int updated = 0; public void updateEntities(I id) { + updated = 0; PageLink pageLink = new PageLink(DEFAULT_LIMIT); boolean hasNext = true; while (hasNext) { @@ -32,13 +36,25 @@ public abstract class PaginatedUpdater DEFAULT_LIMIT || forceReportTotal()) { + log.info("{}: {} total entities updated.", getName(), updated); + } } } } + protected boolean forceReportTotal() { + return false; + } + + protected abstract String getName(); + protected abstract PageData findEntities(I id, PageLink pageLink); protected abstract void updateEntity(D entity); diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index b4c1058616..d11f2bd63b 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -309,6 +309,8 @@ public class DefaultMailService implements MailService { case EMAIL: case SMS: return "send"; + case ALARM: + return "create"; default: throw new RuntimeException("Not implemented!"); } @@ -327,6 +329,8 @@ public class DefaultMailService implements MailService { case EMAIL: case SMS: return "sent"; + case ALARM: + return "created"; default: throw new RuntimeException("Not implemented!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java index 63c587405a..15ca6d7be3 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java @@ -44,6 +44,7 @@ import java.util.Base64; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_SEARCH_TEXT; @@ -130,7 +131,7 @@ public class DefaultTbResourceService implements TbResourceService { List resources = resourceService.findTenantResourcesByResourceTypeAndObjectIds(tenantId, ResourceType.LWM2M_MODEL, objectIds); return resources.stream() - .map(this::toLwM2mObject) + .flatMap(s -> Stream.ofNullable(toLwM2mObject(s))) .sorted(getComparator(sortProperty, sortOrder)) .collect(Collectors.toList()); } @@ -141,7 +142,7 @@ public class DefaultTbResourceService implements TbResourceService { validateId(tenantId, INCORRECT_TENANT_ID + tenantId); PageData resourcePageData = resourceService.findTenantResourcesByResourceTypeAndPageLink(tenantId, ResourceType.LWM2M_MODEL, pageLink); return resourcePageData.getData().stream() - .map(this::toLwM2mObject) + .flatMap(s -> Stream.ofNullable(toLwM2mObject(s))) .sorted(getComparator(sortProperty, sortOrder)) .collect(Collectors.toList()); } @@ -190,9 +191,14 @@ public class DefaultTbResourceService implements TbResourceService { resources.add(lwM2MResourceObserve); } }); - instance.setResources(resources.toArray(LwM2mResourceObserve[]::new)); - lwM2mObject.setInstances(new LwM2mInstance[]{instance}); - return lwM2mObject; + if (resources.size() > 0) { + instance.setResources(resources.toArray(LwM2mResourceObserve[]::new)); + lwM2mObject.setInstances(new LwM2mInstance[]{instance}); + return lwM2mObject; + } + else { + return null; + } } } catch (IOException | InvalidDDFFileException e) { log.error("Could not parse the XML of objectModel with name [{}]", resource.getSearchText(), e); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java index c2ff292667..d85ac8d736 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; @@ -43,6 +44,8 @@ import org.thingsboard.server.dao.alarm.AlarmOperationResult; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.usagestats.TbApiUsageClient; +import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.queue.TbClusterService; import org.thingsboard.server.service.subscription.SubscriptionManagerService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; @@ -58,12 +61,18 @@ import java.util.Optional; public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService implements AlarmSubscriptionService { private final AlarmService alarmService; + private final TbApiUsageClient apiUsageClient; + private final TbApiUsageStateService apiUsageStateService; public DefaultAlarmSubscriptionService(TbClusterService clusterService, PartitionService partitionService, - AlarmService alarmService) { + AlarmService alarmService, + TbApiUsageClient apiUsageClient, + TbApiUsageStateService apiUsageStateService) { super(clusterService, partitionService); this.alarmService = alarmService; + this.apiUsageClient = apiUsageClient; + this.apiUsageStateService = apiUsageStateService; } @Autowired(required = false) @@ -78,10 +87,13 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService @Override public Alarm createOrUpdateAlarm(Alarm alarm) { - AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm); + AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm, apiUsageStateService.getApiUsageState(alarm.getTenantId()).isAlarmCreationEnabled()); if (result.isSuccessful()) { onAlarmUpdated(result); } + if (result.isCreated()) { + apiUsageClient.report(alarm.getTenantId(), null, ApiUsageRecordKey.CREATED_ALARMS_COUNT); + } return result.getAlarm(); } @@ -127,6 +139,11 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService return alarmService.findAlarms(tenantId, query); } + @Override + public ListenableFuture> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) { + return alarmService.findCustomerAlarms(tenantId, customerId, query); + } + @Override public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus) { return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus); diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index b2f19748b6..5f2a47f7ba 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -26,7 +26,7 @@ - + diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index ae7fbda6d9..7fde122cf6 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -673,6 +673,8 @@ transport: recommended_supported_groups: "${LWM2M_RECOMMENDED_SUPPORTED_GROUPS:true}" response_pool_size: "${LWM2M_RESPONSE_POOL_SIZE:100}" registered_pool_size: "${LWM2M_REGISTERED_POOL_SIZE:10}" + registration_store_pool_size: "${LWM2M_REGISTRATION_STORE_POOL_SIZE:100}" + clean_period_in_sec: "${LWM2M_CLEAN_PERIOD_IN_SEC:2}" update_registered_pool_size: "${LWM2M_UPDATE_REGISTERED_POOL_SIZE:10}" un_registered_pool_size: "${LWM2M_UN_REGISTERED_POOL_SIZE:10}" log_max_length: "${LWM2M_LOG_MAX_LENGTH:100}" diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java index 310fb40215..64232e21a6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java @@ -313,7 +313,8 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController Collections.sort(loadedDeviceProfileInfos, deviceProfileInfoIdComparator); List deviceProfileInfos = deviceProfiles.stream().map(deviceProfile -> new DeviceProfileInfo(deviceProfile.getId(), - deviceProfile.getName(), deviceProfile.getType(), deviceProfile.getTransportType())).collect(Collectors.toList()); + deviceProfile.getName(), deviceProfile.getImage(), deviceProfile.getDefaultDashboardId(), + deviceProfile.getType(), deviceProfile.getTransportType())).collect(Collectors.toList()); Assert.assertEquals(deviceProfileInfos, loadedDeviceProfileInfos); diff --git a/application/src/test/java/org/thingsboard/server/service/resource/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/BaseTbResourceServiceTest.java index ffdefc27ed..464ca5c3cf 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/BaseTbResourceServiceTest.java @@ -32,7 +32,6 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.service.AbstractServiceTest; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; @@ -57,7 +56,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { "\n" + "\n" + "LWM2M\n" + - "\n" + + "RW\n" + "Single\n" + "Mandatory\n" + "String\n" + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 624a1d6a72..90b926e78a 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -177,7 +177,7 @@ public class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest { executor = Executors.newScheduledThreadPool(10); loginTenantAdmin(); - String[] resources = new String[]{"0.xml", "1.xml", "2.xml", "3.xml"}; + String[] resources = new String[]{"1.xml", "2.xml", "3.xml"}; for (String resourceName : resources) { TbResource lwModel = new TbResource(); lwModel.setResourceType(ResourceType.LWM2M_MODEL); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java index 14f6572697..7515f31615 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java @@ -26,17 +26,21 @@ import java.util.List; public class AlarmOperationResult { private final Alarm alarm; private final boolean successful; + private final boolean created; private final List propagatedEntitiesList; public AlarmOperationResult(Alarm alarm, boolean successful) { - this.alarm = alarm; - this.successful = successful; - this.propagatedEntitiesList = Collections.emptyList(); + this(alarm, successful, Collections.emptyList()); } public AlarmOperationResult(Alarm alarm, boolean successful, List propagatedEntitiesList) { + this(alarm, successful, false, propagatedEntitiesList); + } + + public AlarmOperationResult(Alarm alarm, boolean successful, boolean created, List propagatedEntitiesList) { this.alarm = alarm; this.successful = successful; + this.created = created; this.propagatedEntitiesList = propagatedEntitiesList; } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index 5badd872df..c0cddd9337 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -29,7 +29,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; -import org.thingsboard.server.common.data.query.AlarmDataPageLink; import org.thingsboard.server.common.data.query.AlarmDataQuery; import java.util.Collection; @@ -41,6 +40,8 @@ public interface AlarmService { AlarmOperationResult createOrUpdateAlarm(Alarm alarm); + AlarmOperationResult createOrUpdateAlarm(Alarm alarm, boolean alarmCreationEnabled); + AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId); ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); @@ -53,6 +54,8 @@ public interface AlarmService { ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query); + ListenableFuture> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); + AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java index 09c05e3c0c..e6a9538e54 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java @@ -28,6 +28,8 @@ public interface EntityService { ListenableFuture fetchEntityNameAsync(TenantId tenantId, EntityId entityId); + CustomerId fetchEntityCustomerId(TenantId tenantId, EntityId entityId); + void deleteEntityRelations(TenantId tenantId, EntityId entityId); long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ApiFeature.java b/common/data/src/main/java/org/thingsboard/server/common/data/ApiFeature.java index cb367b0b93..37206d924a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ApiFeature.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ApiFeature.java @@ -23,7 +23,8 @@ public enum ApiFeature { RE("ruleEngineApiState", "Rule Engine execution"), JS("jsExecutionApiState", "JavaScript functions execution"), EMAIL("emailApiState", "Email messages"), - SMS("smsApiState", "SMS messages"); + SMS("smsApiState", "SMS messages"), + ALARM("alarmApiState", "Created alarms"); @Getter private final String apiStateKey; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageRecordKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageRecordKey.java index f43fe9bf8f..649e62fd42 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageRecordKey.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageRecordKey.java @@ -25,13 +25,16 @@ public enum ApiUsageRecordKey { RE_EXEC_COUNT(ApiFeature.RE, "ruleEngineExecutionCount", "ruleEngineExecutionLimit"), JS_EXEC_COUNT(ApiFeature.JS, "jsExecutionCount", "jsExecutionLimit"), EMAIL_EXEC_COUNT(ApiFeature.EMAIL, "emailCount", "emailLimit"), - SMS_EXEC_COUNT(ApiFeature.SMS, "smsCount", "smsLimit"); + SMS_EXEC_COUNT(ApiFeature.SMS, "smsCount", "smsLimit"), + CREATED_ALARMS_COUNT(ApiFeature.ALARM, "createdAlarmsCount", "createdAlarmsLimit"); + private static final ApiUsageRecordKey[] JS_RECORD_KEYS = {JS_EXEC_COUNT}; private static final ApiUsageRecordKey[] RE_RECORD_KEYS = {RE_EXEC_COUNT}; private static final ApiUsageRecordKey[] DB_RECORD_KEYS = {STORAGE_DP_COUNT}; private static final ApiUsageRecordKey[] TRANSPORT_RECORD_KEYS = {TRANSPORT_MSG_COUNT, TRANSPORT_DP_COUNT}; private static final ApiUsageRecordKey[] EMAIL_RECORD_KEYS = {EMAIL_EXEC_COUNT}; private static final ApiUsageRecordKey[] SMS_RECORD_KEYS = {SMS_EXEC_COUNT}; + private static final ApiUsageRecordKey[] ALARM_RECORD_KEYS = {CREATED_ALARMS_COUNT}; @Getter private final ApiFeature apiFeature; @@ -60,6 +63,8 @@ public enum ApiUsageRecordKey { return EMAIL_RECORD_KEYS; case SMS: return SMS_RECORD_KEYS; + case ALARM: + return ALARM_RECORD_KEYS; default: return new ApiUsageRecordKey[]{}; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java index 4a7067d4df..b8cad06037 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java @@ -25,34 +25,21 @@ import org.thingsboard.server.common.data.id.ApiUsageStateId; @ToString @EqualsAndHashCode(callSuper = true) +@Getter +@Setter public class ApiUsageState extends BaseData implements HasTenantId { private static final long serialVersionUID = 8250339805336035966L; - @Getter - @Setter private TenantId tenantId; - @Getter - @Setter private EntityId entityId; - @Getter - @Setter private ApiUsageStateValue transportState; - @Getter - @Setter private ApiUsageStateValue dbStorageState; - @Getter - @Setter private ApiUsageStateValue reExecState; - @Getter - @Setter private ApiUsageStateValue jsExecState; - @Getter - @Setter private ApiUsageStateValue emailExecState; - @Getter - @Setter private ApiUsageStateValue smsExecState; + private ApiUsageStateValue alarmExecState; public ApiUsageState() { super(); @@ -72,6 +59,7 @@ public class ApiUsageState extends BaseData implements HasTenan this.jsExecState = ur.getJsExecState(); this.emailExecState = ur.getEmailExecState(); this.smsExecState = ur.getSmsExecState(); + this.alarmExecState = ur.getAlarmExecState(); } public boolean isTransportEnabled() { @@ -97,4 +85,8 @@ public class ApiUsageState extends BaseData implements HasTenan public boolean isSmsSendEnabled(){ return !ApiUsageStateValue.DISABLED.equals(smsExecState); } + + public boolean isAlarmCreationEnabled() { + return alarmExecState != ApiUsageStateValue.DISABLED; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java index 854c5ff64b..cbcd213377 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java @@ -30,6 +30,7 @@ public class DashboardInfo extends SearchTextBased implements HasNa private TenantId tenantId; @NoXss private String title; + private String image; @Valid private Set assignedCustomers; @@ -45,6 +46,7 @@ public class DashboardInfo extends SearchTextBased implements HasNa super(dashboardInfo); this.tenantId = dashboardInfo.getTenantId(); this.title = dashboardInfo.getTitle(); + this.image = dashboardInfo.getImage(); this.assignedCustomers = dashboardInfo.getAssignedCustomers(); } @@ -64,6 +66,14 @@ public class DashboardInfo extends SearchTextBased implements HasNa this.title = title; } + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + public Set getAssignedCustomers() { return assignedCustomers; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index f4b5fef1f9..62459fc0ad 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -92,26 +92,6 @@ public class DataConstants { public static final String CLIENT_ID = "clientId"; public static final String USERNAME = "username"; public static final String PASSWORD = "password"; - -//<<<<<<< HEAD -//======= -// //firmware -// //telemetry -// public static final String CURRENT_FIRMWARE_TITLE = "current_fw_title"; -// public static final String CURRENT_FIRMWARE_VERSION = "current_fw_version"; -// public static final String TARGET_FIRMWARE_TITLE = "target_fw_title"; -// public static final String TARGET_FIRMWARE_VERSION = "target_fw_version"; -// public static final String TARGET_FIRMWARE_TS = "target_fw_ts"; -// public static final String FIRMWARE_STATE = "fw_state"; -// -// //attributes -// //telemetry -// public static final String FIRMWARE_TITLE = "fw_title"; -// public static final String FIRMWARE_VERSION = "fw_version"; -// public static final String FIRMWARE_SIZE = "fw_size"; -// public static final String FIRMWARE_CHECKSUM = "fw_checksum"; -// public static final String FIRMWARE_CHECKSUM_ALGORITHM = "fw_checksum_algorithm"; -//>>>>>>> origin/master public static final String EDGE_MSG_SOURCE = "edge"; public static final String MSG_SOURCE_KEY = "source"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java index daf7d77af0..e35fbb84a0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -21,6 +21,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.FirmwareId; import org.thingsboard.server.common.data.id.RuleChainId; @@ -43,11 +44,13 @@ public class DeviceProfile extends SearchTextBased implements H private String name; @NoXss private String description; + private String image; private boolean isDefault; private DeviceProfileType type; private DeviceTransportType transportType; private DeviceProfileProvisionType provisionType; private RuleChainId defaultRuleChainId; + private DashboardId defaultDashboardId; @NoXss private String defaultQueueName; @Valid @@ -74,8 +77,10 @@ public class DeviceProfile extends SearchTextBased implements H this.tenantId = deviceProfile.getTenantId(); this.name = deviceProfile.getName(); this.description = deviceProfile.getDescription(); + this.image = deviceProfile.getImage(); this.isDefault = deviceProfile.isDefault(); this.defaultRuleChainId = deviceProfile.getDefaultRuleChainId(); + this.defaultDashboardId = deviceProfile.getDefaultDashboardId(); this.defaultQueueName = deviceProfile.getDefaultQueueName(); this.setProfileData(deviceProfile.getProfileData()); this.provisionDeviceKey = deviceProfile.getProvisionDeviceKey(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java index db83c58d13..7141382749 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.Value; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -30,21 +31,29 @@ import java.util.UUID; @ToString(callSuper = true) public class DeviceProfileInfo extends EntityInfo { + private final String image; + private final DashboardId defaultDashboardId; private final DeviceProfileType type; private final DeviceTransportType transportType; @JsonCreator public DeviceProfileInfo(@JsonProperty("id") EntityId id, @JsonProperty("name") String name, + @JsonProperty("image") String image, + @JsonProperty("defaultDashboardId") DashboardId defaultDashboardId, @JsonProperty("type") DeviceProfileType type, @JsonProperty("transportType") DeviceTransportType transportType) { super(id, name); + this.image = image; + this.defaultDashboardId = defaultDashboardId; this.type = type; this.transportType = transportType; } - public DeviceProfileInfo(UUID uuid, String name, DeviceProfileType type, DeviceTransportType transportType) { + public DeviceProfileInfo(UUID uuid, String name, String image, UUID defaultDashboardId, DeviceProfileType type, DeviceTransportType transportType) { super(EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE_PROFILE, uuid), name); + this.image = image; + this.defaultDashboardId = defaultDashboardId != null ? new DashboardId(defaultDashboardId) : null; this.type = type; this.transportType = transportType; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java index 32d0a516eb..54f7ab72f5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java @@ -21,9 +21,11 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -35,9 +37,10 @@ import java.util.List; @Data @Builder @AllArgsConstructor -public class Alarm extends BaseData implements HasName, HasTenantId { +public class Alarm extends BaseData implements HasName, HasTenantId, HasCustomerId { private TenantId tenantId; + private CustomerId customerId; private String type; private EntityId originator; private AlarmSeverity severity; @@ -62,6 +65,7 @@ public class Alarm extends BaseData implements HasName, HasTenantId { super(alarm.getId()); this.createdTime = alarm.getCreatedTime(); this.tenantId = alarm.getTenantId(); + this.customerId = alarm.getCustomerId(); this.type = alarm.getType(); this.originator = alarm.getOriginator(); this.severity = alarm.getSeverity(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java index 6373d41803..7b634bffdc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java @@ -82,7 +82,7 @@ public class MqttTopics { public static final String DEVICE_FIRMWARE_REQUEST_TOPIC_PATTERN = BASE_DEVICE_API_TOPIC_V2 + FIRMWARE + REQUEST + "/" + REQUEST_ID_PATTERN + CHUNK + CHUNK_PATTERN; public static final String DEVICE_FIRMWARE_RESPONSES_TOPIC = BASE_DEVICE_API_TOPIC_V2 + FIRMWARE + RESPONSE + "/" + SUB_TOPIC + CHUNK + SUB_TOPIC; public static final String DEVICE_FIRMWARE_ERROR_TOPIC = BASE_DEVICE_API_TOPIC_V2 + FIRMWARE + ERROR; - public static final String DEVICE_FIRMWARE_RESPONSES_TOPIC_FORMAT = BASE_DEVICE_API_TOPIC_V2 + "%s" + RESPONSE + "/"+ "%s" + CHUNK + "%d"; + public static final String DEVICE_FIRMWARE_RESPONSES_TOPIC_FORMAT = BASE_DEVICE_API_TOPIC_V2 + "/%s" + RESPONSE + "/"+ "%s" + CHUNK + "%d"; public static final String DEVICE_SOFTWARE_REQUEST_TOPIC_PATTERN = BASE_DEVICE_API_TOPIC_V2 + SOFTWARE + REQUEST + "/" + REQUEST_ID_PATTERN + CHUNK + CHUNK_PATTERN; public static final String DEVICE_SOFTWARE_RESPONSES_TOPIC = BASE_DEVICE_API_TOPIC_V2 + SOFTWARE + RESPONSE + "/" + SUB_TOPIC + CHUNK + SUB_TOPIC; diff --git a/application/src/main/java/org/thingsboard/server/service/firmware/FirmwareUpdateStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/firmware/FirmwareUpdateStatus.java similarity index 93% rename from application/src/main/java/org/thingsboard/server/service/firmware/FirmwareUpdateStatus.java rename to common/data/src/main/java/org/thingsboard/server/common/data/firmware/FirmwareUpdateStatus.java index ffaab79caf..3e46174792 100644 --- a/application/src/main/java/org/thingsboard/server/service/firmware/FirmwareUpdateStatus.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/firmware/FirmwareUpdateStatus.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.firmware; +package org.thingsboard.server.common.data.firmware; public enum FirmwareUpdateStatus { QUEUED, INITIATED, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATING, UPDATED, FAILED diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 72713f12d3..b9bd72b0db 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -50,6 +50,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private int maxRuleNodeExecutionsPerMessage; private long maxEmails; private long maxSms; + private long maxCreatedAlarms; private int defaultStorageTtlDays; @@ -72,6 +73,8 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura return maxEmails; case SMS_EXEC_COUNT: return maxSms; + case CREATED_ALARMS_COUNT: + return maxCreatedAlarms; } return 0L; } diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index aae103fc4f..1b639af89d 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -52,6 +52,10 @@ org.springframework spring-context + + org.springframework.integration + spring-integration-redis + org.slf4j slf4j-api diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java index b1edd0a1a5..ccc2e62117 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java @@ -46,6 +46,7 @@ import java.util.UUID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.BOOTSTRAP_SERVER; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_ERROR; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_INFO; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_TELEMETRY; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LWM2M_SERVER; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SERVERS; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.getBootstrapParametersFromThingsboard; @@ -165,13 +166,13 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { lwM2MBootstrapConfig.bootstrapServer = new LwM2MServerBootstrap(lwM2MBootstrapConfig.bootstrapServer, profileServerBootstrap); lwM2MBootstrapConfig.lwm2mServer = new LwM2MServerBootstrap(lwM2MBootstrapConfig.lwm2mServer, profileLwm2mServer); String logMsg = String.format("%s: getParametersBootstrap: %s Access connect client with bootstrap server.", LOG_LW2M_INFO, store.getEndpoint()); - helper.sendParametersOnThingsboardTelemetry(helper.getKvLogyToThingsboard(logMsg), sessionInfo); + helper.sendParametersOnThingsboardTelemetry(helper.getKvStringtoThingsboard(LOG_LW2M_TELEMETRY, logMsg), sessionInfo); return lwM2MBootstrapConfig; } else { log.error(" [{}] Different values SecurityMode between of client and profile.", store.getEndpoint()); log.error("{} getParametersBootstrap: [{}] Different values SecurityMode between of client and profile.", LOG_LW2M_ERROR, store.getEndpoint()); String logMsg = String.format("%s: getParametersBootstrap: %s Different values SecurityMode between of client and profile.", LOG_LW2M_ERROR, store.getEndpoint()); - helper.sendParametersOnThingsboardTelemetry(helper.getKvLogyToThingsboard(logMsg), sessionInfo); + helper.sendParametersOnThingsboardTelemetry(helper.getKvStringtoThingsboard(LOG_LW2M_TELEMETRY, logMsg), sessionInfo); return null; } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java index 109898812b..25c7766895 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java @@ -64,6 +64,14 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { @Value("${transport.lwm2m.registered_pool_size:}") private int registeredPoolSize; + @Getter + @Value("${transport.lwm2m.registration_store_pool_size:}") + private int registrationStorePoolSize; + + @Getter + @Value("${transport.lwm2m.clean_period_in_sec:}") + private int cleanPeriodInSec; + @Getter @Value("${transport.lwm2m.update_registered_pool_size:}") private int updateRegisteredPoolSize; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java index c2a58f1cec..8466459b91 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java @@ -23,13 +23,12 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; -import org.eclipse.leshan.core.node.LwM2mMultipleResource; import org.eclipse.leshan.core.node.LwM2mObject; import org.eclipse.leshan.core.node.LwM2mObjectInstance; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; -import org.eclipse.leshan.core.node.LwM2mSingleResource; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.WriteRequest; @@ -56,6 +55,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.SessionEvent; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper; import org.thingsboard.server.transport.lwm2m.server.adaptors.LwM2MJsonAdaptor; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; @@ -85,19 +85,21 @@ import java.util.stream.Collectors; import static org.eclipse.californium.core.coap.CoAP.ResponseCode.BAD_REQUEST; import static org.eclipse.leshan.core.attributes.Attribute.OBJECT_VERSION; -import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; +import static org.thingsboard.server.common.data.firmware.FirmwareUpdateStatus.DOWNLOADED; +import static org.thingsboard.server.common.data.firmware.FirmwareUpdateStatus.UPDATING; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper.getValueFromKvProto; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.CLIENT_NOT_AUTHORIZED; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.DEVICE_ATTRIBUTES_REQUEST; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FR_OBJECT_ID; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FR_PATH_RESOURCE_VER_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_RESULT_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_ERROR; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_INFO; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_TELEMETRY; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_VALUE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LWM2M_STRATEGY_2; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.DISCOVER; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.DISCOVER_All; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.EXECUTE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.OBSERVE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.OBSERVE_CANCEL; @@ -105,6 +107,9 @@ import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.L import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.READ; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_ATTRIBUTES; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_REPLACE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_UPDATE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_RESULT_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertJsonArrayToSet; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromIdVerToObjectId; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromObjectIdToIdVer; @@ -119,19 +124,20 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler private ExecutorService registrationExecutor; private ExecutorService updateRegistrationExecutor; - private ExecutorService unregistrationExecutor; + private ExecutorService unRegistrationExecutor; private LwM2mValueConverterImpl converter; private final TransportService transportService; private final LwM2mTransportContext context; - private final LwM2MTransportServerConfig config; - private final FirmwareDataCache firmwareDataCache; - private final LwM2mTransportServerHelper helper; + public final LwM2MTransportServerConfig config; + public final FirmwareDataCache firmwareDataCache; + public final LwM2mTransportServerHelper helper; private final LwM2MJsonAdaptor adaptor; private final LwM2mClientContext clientContext; private final LwM2mTransportRequest lwM2mTransportRequest; private final TbLwM2MDtlsSessionStore sessionStore; + public DefaultLwM2MTransportMsgHandler(TransportService transportService, LwM2MTransportServerConfig config, LwM2mTransportServerHelper helper, LwM2mClientContext clientContext, @Lazy LwM2mTransportRequest lwM2mTransportRequest, @@ -153,7 +159,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler this.context.getScheduler().scheduleAtFixedRate(this::reportActivity, new Random().nextInt((int) config.getSessionReportTimeout()), config.getSessionReportTimeout(), TimeUnit.MILLISECONDS); this.registrationExecutor = ThingsBoardExecutors.newWorkStealingPool(this.config.getRegisteredPoolSize(), "LwM2M registration"); this.updateRegistrationExecutor = ThingsBoardExecutors.newWorkStealingPool(this.config.getUpdateRegisteredPoolSize(), "LwM2M update registration"); - this.unregistrationExecutor = ThingsBoardExecutors.newWorkStealingPool(this.config.getUnRegisteredPoolSize(), "LwM2M unregistration"); + this.unRegistrationExecutor = ThingsBoardExecutors.newWorkStealingPool(this.config.getUnRegisteredPoolSize(), "LwM2M unRegistration"); this.converter = LwM2mValueConverterImpl.getInstance(); } @@ -184,6 +190,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), null); transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.newBuilder().build(), null); this.getInfoFirmwareUpdate(lwM2MClient); + this.getInfoSoftwareUpdate(lwM2MClient); this.initLwM2mFromClientValue(registration, lwM2MClient); this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client create after Registration", registration.getId()); } else { @@ -216,11 +223,14 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler request.send(); } } + this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client update Registration", registration.getId()); } else { log.error("Client: [{}] updatedReg [{}] name [{}] sessionInfo ", registration.getId(), registration.getEndpoint(), null); + this.sendLogsToThingsboard(LOG_LW2M_ERROR + ": Client update Registration", registration.getId()); } } catch (Throwable t) { log.error("[{}] endpoint [{}] error Unable update registration.", registration.getEndpoint(), t); + this.sendLogsToThingsboard(LOG_LW2M_ERROR + String.format(": Client update Registration, %s", t.getMessage()), registration.getId()); } }); } @@ -231,13 +241,15 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler * !!! Warn: if have not finishing unReg, then this operation will be finished on next Client`s connect */ public void unReg(Registration registration, Collection observations) { - unregistrationExecutor.submit(() -> { + unRegistrationExecutor.submit(() -> { try { - this.setCancelObservations(registration); + this.setCancelObservationsAll(registration); this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client unRegistration", registration.getId()); this.closeClientSession(registration); + ; } catch (Throwable t) { log.error("[{}] endpoint [{}] error Unable un registration.", registration.getEndpoint(), t); + this.sendLogsToThingsboard(LOG_LW2M_ERROR + String.format(": Client Unable un Registration, %s", t.getMessage()), registration.getId()); } }); } @@ -263,13 +275,18 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler //TODO: associate endpointId with device information. } + /** + * Cancel observation for All objects for this registration + */ @Override - public void setCancelObservations(Registration registration) { + public void setCancelObservationsAll(Registration registration) { if (registration != null) { - Set observations = context.getServer().getObservationService().getObservations(registration); - observations.forEach(observation -> lwM2mTransportRequest.sendAllRequest(registration, - convertPathFromObjectIdToIdVer(observation.getPath().toString(), registration), OBSERVE_CANCEL, - null, null, this.config.getTimeout(), null)); + lwM2mTransportRequest.sendAllRequest(registration, null, OBSERVE_CANCEL, + null, null, this.config.getTimeout(), null); +// Set observations = context.getServer().getObservationService().getObservations(registration); +// observations.forEach(observation -> lwM2mTransportRequest.sendAllRequest(registration, +// convertPathFromObjectIdToIdVer(observation.getPath().toString(), registration), OBSERVE_CANCEL, +// null, null, this.config.getTimeout(), null)); } } @@ -283,34 +300,43 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler @Override public void onUpdateValueAfterReadResponse(Registration registration, String path, ReadResponse response, Lwm2mClientRpcRequest rpcRequest) { if (response.getContent() != null) { - Object value = null; - if (response.getContent() instanceof LwM2mObject) { - LwM2mObject lwM2mObject = (LwM2mObject) response.getContent(); - if (rpcRequest != null) { - value = lwM2mObject.toString(); + LwM2mClient lwM2MClient = clientContext.getOrRegister(registration); + ObjectModel objectModelVersion = lwM2MClient.getObjectModel(path, this.config.getModelProvider()); + if (objectModelVersion != null) { + if (response.getContent() instanceof LwM2mObject) { + LwM2mObject lwM2mObject = (LwM2mObject) response.getContent(); + this.updateObjectResourceValue(registration, lwM2mObject, path); + } else if (response.getContent() instanceof LwM2mObjectInstance) { + LwM2mObjectInstance lwM2mObjectInstance = (LwM2mObjectInstance) response.getContent(); + this.updateObjectInstanceResourceValue(registration, lwM2mObjectInstance, path); + } else if (response.getContent() instanceof LwM2mResource) { + LwM2mResource lwM2mResource = (LwM2mResource) response.getContent(); + this.updateResourcesValue(registration, lwM2mResource, path); } - this.updateObjectResourceValue(registration, lwM2mObject, path); - } else if (response.getContent() instanceof LwM2mObjectInstance) { - LwM2mObjectInstance lwM2mObjectInstance = (LwM2mObjectInstance) response.getContent(); - if (rpcRequest != null) { - value = lwM2mObjectInstance.toString(); - } - this.updateObjectInstanceResourceValue(registration, lwM2mObjectInstance, path); - } else if (response.getContent() instanceof LwM2mResource) { - LwM2mResource lwM2mResource = (LwM2mResource) response.getContent(); - if (rpcRequest != null) { - value = lwM2mResource.isMultiInstances() ? ((LwM2mMultipleResource) lwM2mResource).toString() : - ((LwM2mSingleResource) lwM2mResource).toString(); - } - this.updateResourcesValue(registration, lwM2mResource, path); } if (rpcRequest != null) { - rpcRequest.setValueMsg(String.format("%s", value)); - this.sentRpcRequest(rpcRequest, response.getCode().getName(), (String) value, LOG_LW2M_VALUE); + this.sendRpcRequestAfterReadResponse(registration, lwM2MClient, path, response, rpcRequest); } } } + private void sendRpcRequestAfterReadResponse(Registration registration, LwM2mClient lwM2MClient, String pathIdVer, ReadResponse response, + Lwm2mClientRpcRequest rpcRequest) { + Object value = null; + if (response.getContent() instanceof LwM2mObject) { + value = lwM2MClient.objectToString((LwM2mObject) response.getContent(), this.converter, pathIdVer); + } else if (response.getContent() instanceof LwM2mObjectInstance) { + value = lwM2MClient.instanceToString((LwM2mObjectInstance) response.getContent(), this.converter, pathIdVer); + } else if (response.getContent() instanceof LwM2mResource) { + value = lwM2MClient.resourceToString((LwM2mResource) response.getContent(), this.converter, pathIdVer); + } + String msg = String.format("%s: type operation %s path - %s value - %s", LOG_LW2M_INFO, + READ, pathIdVer, value); + this.sendLogsToThingsboard(msg, registration.getId()); + rpcRequest.setValueMsg(String.format("%s", value)); + this.sentRpcRequest(rpcRequest, response.getCode().getName(), (String) value, LOG_LW2M_VALUE); + } + /** * Update - send request in change value resources in Client * 1. FirmwareUpdate: @@ -330,9 +356,16 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler String pathName = tsKvProto.getKv().getKey(); String pathIdVer = this.getPresentPathIntoProfile(sessionInfo, pathName); Object valueNew = getValueFromKvProto(tsKvProto.getKv()); - //TODO: react on change of the firmware name. - if (FirmwareUtil.getAttributeKey(FirmwareType.FIRMWARE, FirmwareKey.VERSION).equals(pathName) && !valueNew.equals(lwM2MClient.getFrUpdate().getCurrentFwVersion())) { + if ((FirmwareUtil.getAttributeKey(FirmwareType.FIRMWARE, FirmwareKey.VERSION).equals(pathName) + && (!valueNew.equals(lwM2MClient.getFwUpdate().getCurrentVersion()))) + || (FirmwareUtil.getAttributeKey(FirmwareType.FIRMWARE, FirmwareKey.TITLE).equals(pathName) + && (!valueNew.equals(lwM2MClient.getFwUpdate().getCurrentTitle())))) { this.getInfoFirmwareUpdate(lwM2MClient); + } else if ((FirmwareUtil.getAttributeKey(FirmwareType.SOFTWARE, FirmwareKey.VERSION).equals(pathName) + && (!valueNew.equals(lwM2MClient.getSwUpdate().getCurrentVersion()))) + || (FirmwareUtil.getAttributeKey(FirmwareType.SOFTWARE, FirmwareKey.TITLE).equals(pathName) + && (!valueNew.equals(lwM2MClient.getSwUpdate().getCurrentTitle())))) { + this.getInfoSoftwareUpdate(lwM2MClient); } if (pathIdVer != null) { ResourceModel resourceModel = lwM2MClient.getResourceModel(pathIdVer, this.config @@ -357,8 +390,8 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler msg.getSharedUpdatedList().forEach(tsKvProto -> { String pathName = tsKvProto.getKv().getKey(); Object valueNew = getValueFromKvProto(tsKvProto.getKv()); - if (FirmwareUtil.getAttributeKey(FirmwareType.FIRMWARE, FirmwareKey.VERSION).equals(pathName) && !valueNew.equals(lwM2MClient.getFrUpdate().getCurrentFwVersion())) { - lwM2MClient.getFrUpdate().setCurrentFwVersion((String) valueNew); + if (FirmwareUtil.getAttributeKey(FirmwareType.FIRMWARE, FirmwareKey.VERSION).equals(pathName) && !valueNew.equals(lwM2MClient.getFwUpdate().getCurrentVersion())) { + lwM2MClient.getFwUpdate().setCurrentVersion((String) valueNew); } }); log.info("[{}] delete [{}] onAttributeUpdate", msg.getSharedDeletedList(), sessionInfo); @@ -402,12 +435,12 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler } @Override - public void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg toDeviceRequest, SessionInfoProto sessionInfo) { + public void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg toDeviceRpcRequestMsg, SessionInfoProto sessionInfo) { + log.warn("4) RPC-OK finish to [{}]", toDeviceRpcRequestMsg); Lwm2mClientRpcRequest lwm2mClientRpcRequest = null; try { - log.info("[{}] toDeviceRpcRequest", toDeviceRequest); Registration registration = clientContext.getClient(new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB())).getRegistration(); - lwm2mClientRpcRequest = this.getDeviceRpcRequest(toDeviceRequest, sessionInfo, registration); + lwm2mClientRpcRequest = this.getDeviceRpcRequest(toDeviceRpcRequestMsg, sessionInfo, registration); if (lwm2mClientRpcRequest.getErrorMsg() != null) { lwm2mClientRpcRequest.setResponseCode(BAD_REQUEST.name()); this.onToDeviceRpcResponse(lwm2mClientRpcRequest.getDeviceRpcResponseResultMsg(), sessionInfo); @@ -459,12 +492,27 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler lwm2mClientRpcRequest.setValue(rpcRequest.get(lwm2mClientRpcRequest.valueKey).getAsString()); } if (rpcRequest.has(lwm2mClientRpcRequest.paramsKey) && rpcRequest.get(lwm2mClientRpcRequest.paramsKey).isJsonObject()) { - lwm2mClientRpcRequest.setParams(new Gson().fromJson(rpcRequest.get(lwm2mClientRpcRequest.paramsKey) + ConcurrentHashMap params = new Gson().fromJson(rpcRequest.get(lwm2mClientRpcRequest.paramsKey) + .getAsJsonObject().toString(), new TypeToken>() { + }.getType()); + if (WRITE_UPDATE == lwm2mClientRpcRequest.getTypeOper()) { + ConcurrentHashMap paramsResourceId = convertParamsToResourceId(params, sessionInfo); + if (paramsResourceId.size() > 0) { + lwm2mClientRpcRequest.setParams(paramsResourceId); + } + } else { + lwm2mClientRpcRequest.setParams(params); + } + } else if (rpcRequest.has(lwm2mClientRpcRequest.paramsKey) && rpcRequest.get(lwm2mClientRpcRequest.paramsKey).isJsonArray()) { + new Gson().fromJson(rpcRequest.get(lwm2mClientRpcRequest.paramsKey) .getAsJsonObject().toString(), new TypeToken>() { - }.getType())); + }.getType()); } lwm2mClientRpcRequest.setSessionInfo(sessionInfo); - if (OBSERVE_READ_ALL != lwm2mClientRpcRequest.getTypeOper() && lwm2mClientRpcRequest.getTargetIdVer() == null) { + if (!(OBSERVE_READ_ALL == lwm2mClientRpcRequest.getTypeOper() + || DISCOVER_All == lwm2mClientRpcRequest.getTypeOper() + || OBSERVE_CANCEL == lwm2mClientRpcRequest.getTypeOper()) + && lwm2mClientRpcRequest.getTargetIdVer() == null) { lwm2mClientRpcRequest.setErrorMsg(lwm2mClientRpcRequest.targetIdVerKey + " and " + lwm2mClientRpcRequest.keyNameKey + " is null or bad format"); } @@ -488,6 +536,21 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler return lwm2mClientRpcRequest; } + private ConcurrentHashMap convertParamsToResourceId(ConcurrentHashMap params, + SessionInfoProto sessionInfo) { + ConcurrentHashMap paramsIdVer = new ConcurrentHashMap<>(); + params.forEach((k, v) -> { + String targetIdVer = this.getPresentPathIntoProfile(sessionInfo, k); + if (targetIdVer != null) { + LwM2mPath targetId = new LwM2mPath(convertPathFromIdVerToObjectId(targetIdVer)); + if (targetId.isResource()) { + paramsIdVer.put(String.valueOf(targetId.getResourceId()), v); + } + } + }); + return paramsIdVer; + } + public void sentRpcRequest(Lwm2mClientRpcRequest rpcRequest, String requestCode, String msg, String typeMsg) { rpcRequest.setResponseCode(requestCode); if (LOG_LW2M_ERROR.equals(typeMsg)) { @@ -561,7 +624,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler */ @Override public void onAwakeDev(Registration registration) { - log.info("[{}] [{}] Received endpoint Awake version event", registration.getId(), registration.getEndpoint()); + log.trace("[{}] [{}] Received endpoint Awake version event", registration.getId(), registration.getEndpoint()); this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client is awake!", registration.getId()); //TODO: associate endpointId with device information. } @@ -577,7 +640,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler if (logMsg.length() > 1024) { logMsg = logMsg.substring(0, 1024); } - this.helper.sendParametersOnThingsboardTelemetry(this.helper.getKvLogyToThingsboard(logMsg), sessionInfo); + this.helper.sendParametersOnThingsboardTelemetry(this.helper.getKvStringtoThingsboard(LOG_LW2M_TELEMETRY, logMsg), sessionInfo); } } @@ -614,7 +677,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler /** * @param registration - * @param lwM2mObject - - * @param pathIdVer - + * @param pathIdVer - */ private void updateObjectResourceValue(Registration registration, LwM2mObject lwM2mObject, String pathIdVer) { LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer)); @@ -651,18 +714,63 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler private void updateResourcesValue(Registration registration, LwM2mResource lwM2mResource, String path) { LwM2mClient lwM2MClient = clientContext.getOrRegister(registration); if (lwM2MClient.saveResourceValue(path, lwM2mResource, this.config.getModelProvider())) { - if (FR_PATH_RESOURCE_VER_ID.equals(convertPathFromIdVerToObjectId(path)) && - lwM2MClient.getFrUpdate().getCurrentFwVersion() != null - && !lwM2MClient.getFrUpdate().getCurrentFwVersion().equals(lwM2MClient.getFrUpdate().getClientFwVersion()) - && lwM2MClient.isUpdateFw()) { - - /** version != null - * set setClient_fw_version = value - **/ - lwM2MClient.setUpdateFw(false); - lwM2MClient.getFrUpdate().setClientFwVersion(lwM2mResource.getValue().toString()); - log.warn("updateFirmwareClient3"); - this.updateFirmwareClient(lwM2MClient); + /** version != null + * set setClient_fw_info... = value + **/ + if (lwM2MClient.getFwUpdate().isInfoFwSwUpdate()) { + lwM2MClient.getFwUpdate().initReadValue(this, lwM2mTransportRequest, path); + } + if (lwM2MClient.getSwUpdate().isInfoFwSwUpdate()) { + lwM2MClient.getSwUpdate().initReadValue(this, lwM2mTransportRequest, path); + } + + /** + * Before operation Execute (FwUpdate) inspection Update Result : + * - after finished operation Write result: success (FwUpdate): fw_state = DOWNLOADED + * - before start operation Execute (FwUpdate) Update Result = 0 - Initial value + * - start Execute (FwUpdate) + * After finished operation Execute (FwUpdate) inspection Update Result : + * - after start operation Execute (FwUpdate): fw_state = UPDATING + * - after success finished operation Execute (FwUpdate) Update Result == 1 ("Firmware updated successfully") + * - finished operation Execute (FwUpdate) + */ + if (lwM2MClient.getFwUpdate() != null + && (convertPathFromObjectIdToIdVer(FW_RESULT_ID, registration).equals(path))) { + if (DOWNLOADED.name().equals(lwM2MClient.getFwUpdate().getStateUpdate()) + && lwM2MClient.getFwUpdate().conditionalFwExecuteStart()) { + lwM2MClient.getFwUpdate().executeFwSwWare(this, lwM2mTransportRequest); + } else if (UPDATING.name().equals(lwM2MClient.getFwUpdate().getStateUpdate()) + && lwM2MClient.getFwUpdate().conditionalFwExecuteAfterSuccess()) { + lwM2MClient.getFwUpdate().finishFwSwUpdate(this, true); + } else if (UPDATING.name().equals(lwM2MClient.getFwUpdate().getStateUpdate()) + && lwM2MClient.getFwUpdate().conditionalFwExecuteAfterError()) { + lwM2MClient.getFwUpdate().finishFwSwUpdate(this, false); + } + } + + /** + * Before operation Execute (SwUpdate) inspection Update Result : + * - after finished operation Write result: success (SwUpdate): fw_state = DOWNLOADED + * - before operation Execute (SwUpdate) Update Result = 3 - Successfully Downloaded and package integrity verified + * - start Execute (SwUpdate) + * After finished operation Execute (SwUpdate) inspection Update Result : + * - after start operation Execute (SwUpdate): fw_state = UPDATING + * - after success finished operation Execute (SwUpdate) Update Result == 2 "Software successfully installed."" + * - after success finished operation Execute (SwUpdate) Update Result == 2 "Software successfully installed."" + * - finished operation Execute (SwUpdate) + */ + if (lwM2MClient.getSwUpdate() != null + && (convertPathFromObjectIdToIdVer(SW_RESULT_ID, registration).equals(path))) { + if (DOWNLOADED.name().equals(lwM2MClient.getSwUpdate().getStateUpdate()) + && lwM2MClient.getSwUpdate().conditionalSwUpdateExecute()) { + lwM2MClient.getSwUpdate().executeFwSwWare(this, lwM2mTransportRequest); + } else if (UPDATING.name().equals(lwM2MClient.getSwUpdate().getStateUpdate()) + && lwM2MClient.getSwUpdate().conditionalSwExecuteAfterSuccess()) { + lwM2MClient.getSwUpdate().finishFwSwUpdate(this, true); + } else if (UPDATING.name().equals(lwM2MClient.getSwUpdate().getStateUpdate()) + && lwM2MClient.getSwUpdate().conditionalSwExecuteAfterError()) { + lwM2MClient.getSwUpdate().finishFwSwUpdate(this, false); + } } Set paths = new HashSet<>(); paths.add(path); @@ -684,7 +792,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler */ private void updateAttrTelemetry(Registration registration, Set paths) { try { - ResultsAddKeyValueProto results = getParametersFromProfile(registration, paths); + ResultsAddKeyValueProto results = this.getParametersFromProfile(registration, paths); SessionInfoProto sessionInfo = this.getSessionInfoOrCloseSession(registration); if (results != null && sessionInfo != null) { if (results.getResultAttributes().size() > 0) { @@ -818,7 +926,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler Object finalvalueKvProto = valueKvProto; Gson gson = new GsonBuilder().create(); resourceValue.getValues().forEach((k, v) -> { - Object val = this.converter.convertValue(resourceValue.getValue(), currentType, expectedType, + Object val = this.converter.convertValue(v, currentType, expectedType, new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer))); JsonElement element = gson.toJsonTree(val, val.getClass()); ((JsonObject) finalvalueKvProto).add(String.valueOf(k), element); @@ -910,7 +1018,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler */ private void onDeviceProfileUpdate(Set registrationIds, DeviceProfile deviceProfile) { LwM2mClientProfile lwM2MClientProfileOld = clientContext.getProfiles().get(deviceProfile.getUuidId()).clone(); - if (clientContext.toClientProfile(deviceProfile) != null) { + if (clientContext.profileUpdate(deviceProfile) != null) { // #1 JsonArray attributeOld = lwM2MClientProfileOld.getPostAttributeProfile(); Set attributeSetOld = convertJsonArrayToSet(attributeOld); @@ -920,7 +1028,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler JsonObject keyNameOld = lwM2MClientProfileOld.getPostKeyNameProfile(); JsonObject attributeLwm2mOld = lwM2MClientProfileOld.getPostAttributeLwm2mProfile(); - LwM2mClientProfile lwM2MClientProfileNew = clientContext.getProfiles().get(deviceProfile.getUuidId()); + LwM2mClientProfile lwM2MClientProfileNew = clientContext.getProfiles().get(deviceProfile.getUuidId()).clone(); JsonArray attributeNew = lwM2MClientProfileNew.getPostAttributeProfile(); Set attributeSetNew = convertJsonArrayToSet(attributeNew); JsonArray telemetryNew = lwM2MClientProfileNew.getPostTelemetryProfile(); @@ -968,8 +1076,6 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler registrationIds.forEach(registrationId -> { Registration registration = clientContext.getRegistration(registrationId); this.readObserveFromProfile(registration, sendAttrToThingsboard.getPathPostParametersAdd(), READ); - // send attr/telemetry to tingsboard for new path - this.updateAttrTelemetry(registration, sendAttrToThingsboard.getPathPostParametersAdd()); }); } // #4.2 del @@ -1248,7 +1354,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler * @param registration - Registration LwM2M Client * @return - sessionInfo after access connect client */ - private SessionInfoProto getSessionInfoOrCloseSession(Registration registration) { + public SessionInfoProto getSessionInfoOrCloseSession(Registration registration) { return getSessionInfoOrCloseSession(clientContext.getOrRegister(registration)); } @@ -1296,7 +1402,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler TransportProtos.GetAttributeRequestMsg getAttributeMsg = adaptor.convertToGetAttributes(null, keyNamesMap.values()); transportService.process(sessionInfo, getAttributeMsg, getAckCallback(lwM2MClient, getAttributeMsg.getRequestId(), DEVICE_ATTRIBUTES_REQUEST)); } catch (AdaptorException e) { - log.warn("Failed to decode get attributes request", e); + log.trace("Failed to decode get attributes request", e); } } @@ -1304,62 +1410,72 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler } public void getInfoFirmwareUpdate(LwM2mClient lwM2MClient) { - SessionInfoProto sessionInfo = this.getSessionInfoOrCloseSession(lwM2MClient); - if (sessionInfo != null) { - TransportProtos.GetFirmwareRequestMsg getFirmwareRequestMsg = TransportProtos.GetFirmwareRequestMsg.newBuilder() - .setDeviceIdMSB(sessionInfo.getDeviceIdMSB()) - .setDeviceIdLSB(sessionInfo.getDeviceIdLSB()) - .setTenantIdMSB(sessionInfo.getTenantIdMSB()) - .setTenantIdLSB(sessionInfo.getTenantIdLSB()) - .setType(FirmwareType.FIRMWARE.name()) - .build(); - transportService.process(sessionInfo, getFirmwareRequestMsg, - new TransportServiceCallback<>() { - @Override - public void onSuccess(TransportProtos.GetFirmwareResponseMsg response) { - if (TransportProtos.ResponseStatus.SUCCESS.equals(response.getResponseStatus())) { - lwM2MClient.getFrUpdate().setCurrentFwVersion(response.getVersion()); - lwM2MClient.getFrUpdate().setCurrentFwId(new FirmwareId(new UUID(response.getFirmwareIdMSB(), response.getFirmwareIdLSB())).getId()); - lwM2MClient.setUpdateFw(true); - readRequestToClientFirmwareVer(lwM2MClient.getRegistration()); - } else { - log.trace("Firmware [{}] [{}]", lwM2MClient.getDeviceName(), response.getResponseStatus().toString()); + if (lwM2MClient.getRegistration().getSupportedVersion(FW_ID) != null) { + SessionInfoProto sessionInfo = this.getSessionInfoOrCloseSession(lwM2MClient); + if (sessionInfo != null) { + DefaultLwM2MTransportMsgHandler serviceImpl = this; + transportService.process(sessionInfo, createFirmwareRequestMsg(sessionInfo, FirmwareType.FIRMWARE.name()), + new TransportServiceCallback<>() { + @Override + public void onSuccess(TransportProtos.GetFirmwareResponseMsg response) { + if (TransportProtos.ResponseStatus.SUCCESS.equals(response.getResponseStatus()) + && response.getType().equals(FirmwareType.FIRMWARE.name())) { + lwM2MClient.getFwUpdate().setCurrentVersion(response.getVersion()); + lwM2MClient.getFwUpdate().setCurrentTitle(response.getTitle()); + lwM2MClient.getFwUpdate().setCurrentId(new FirmwareId(new UUID(response.getFirmwareIdMSB(), response.getFirmwareIdLSB())).getId()); + lwM2MClient.getFwUpdate().sendReadObserveInfo(lwM2mTransportRequest); + } else { + log.trace("Firmware [{}] [{}]", lwM2MClient.getDeviceName(), response.getResponseStatus().toString()); + } } - } - @Override - public void onError(Throwable e) { - log.trace("Failed to process credentials ", e); - } - }); + @Override + public void onError(Throwable e) { + log.trace("Failed to process firmwareUpdate ", e); + } + }); + } } } - /** - * @param registration - */ - public void readRequestToClientFirmwareVer(Registration registration) { - String pathIdVer = convertPathFromObjectIdToIdVer(FR_PATH_RESOURCE_VER_ID, registration); - lwM2mTransportRequest.sendAllRequest(registration, pathIdVer, READ, ContentFormat.TLV.getName(), - null, config.getTimeout(), null); - } + public void getInfoSoftwareUpdate(LwM2mClient lwM2MClient) { + if (lwM2MClient.getRegistration().getSupportedVersion(SW_ID) != null) { + SessionInfoProto sessionInfo = this.getSessionInfoOrCloseSession(lwM2MClient); + if (sessionInfo != null) { + DefaultLwM2MTransportMsgHandler serviceImpl = this; + transportService.process(sessionInfo, createFirmwareRequestMsg(sessionInfo, FirmwareType.SOFTWARE.name()), + new TransportServiceCallback<>() { + @Override + public void onSuccess(TransportProtos.GetFirmwareResponseMsg response) { + if (TransportProtos.ResponseStatus.SUCCESS.equals(response.getResponseStatus()) + && response.getType().equals(FirmwareType.SOFTWARE.name())) { + lwM2MClient.getSwUpdate().setCurrentVersion(response.getVersion()); + lwM2MClient.getSwUpdate().setCurrentTitle(response.getTitle()); + lwM2MClient.getSwUpdate().setCurrentId(new FirmwareId(new UUID(response.getFirmwareIdMSB(), response.getFirmwareIdLSB())).getId()); + lwM2MClient.getSwUpdate().sendReadObserveInfo(lwM2mTransportRequest); + } else { + log.trace("Software [{}] [{}]", lwM2MClient.getDeviceName(), response.getResponseStatus().toString()); + } + } - /** - * @param lwM2MClient - - */ - public void updateFirmwareClient(LwM2mClient lwM2MClient) { - if (!lwM2MClient.getFrUpdate().getCurrentFwVersion().equals(lwM2MClient.getFrUpdate().getClientFwVersion())) { - int chunkSize = 0; - int chunk = 0; - byte[] firmwareChunk = firmwareDataCache.get(lwM2MClient.getFrUpdate().getCurrentFwId().toString(), chunkSize, chunk); - String verSupportedObject = lwM2MClient.getRegistration().getSupportedObject().get(FR_OBJECT_ID); - String targetIdVer = LWM2M_SEPARATOR_PATH + FR_OBJECT_ID + LWM2M_SEPARATOR_KEY + verSupportedObject + LWM2M_SEPARATOR_PATH + 0 + LWM2M_SEPARATOR_PATH + 0; - lwM2mTransportRequest.sendAllRequest(lwM2MClient.getRegistration(), targetIdVer, WRITE_REPLACE, ContentFormat.OPAQUE.getName(), - firmwareChunk, config.getTimeout(), null); - log.warn("updateFirmwareClient [{}] [{}]", lwM2MClient.getFrUpdate().getCurrentFwVersion(), lwM2MClient.getFrUpdate().getClientFwVersion()); + @Override + public void onError(Throwable e) { + log.trace("Failed to process softwareUpdate ", e); + } + }); + } } } + private TransportProtos.GetFirmwareRequestMsg createFirmwareRequestMsg(SessionInfoProto sessionInfo, String nameFwSW) { + return TransportProtos.GetFirmwareRequestMsg.newBuilder() + .setDeviceIdMSB(sessionInfo.getDeviceIdMSB()) + .setDeviceIdLSB(sessionInfo.getDeviceIdLSB()) + .setTenantIdMSB(sessionInfo.getTenantIdMSB()) + .setTenantIdLSB(sessionInfo.getTenantIdLSB()) + .setType(nameFwSW) + .build(); + } /** * !!! sharedAttr === profileAttr !!! @@ -1386,4 +1502,8 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler objectId != null && objectVer != null && objectVer.equals(lwM2mClient.getRegistration().getSupportedVersion(objectId))); } + public LwM2MTransportServerConfig getConfig() { + return this.config; + } + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index 8bd0fdaff2..90e3e13033 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -28,6 +28,7 @@ import org.eclipse.leshan.server.californium.registration.CaliforniumRegistratio import org.eclipse.leshan.server.model.LwM2mModelProvider; import org.eclipse.leshan.server.security.EditableSecurityStore; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; @@ -57,6 +58,8 @@ import java.security.spec.InvalidParameterSpecException; import java.security.spec.KeySpec; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Arrays; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256; import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8; @@ -122,7 +125,6 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { /* Use a magic converter to support bad type send by the UI. */ builder.setEncoder(new DefaultLwM2mNodeEncoder(LwM2mValueConverterImpl.getInstance())); - /* Create CoAP Config */ builder.setCoapConfig(getCoapConfig(config.getPort(), config.getSecurePort())); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java index f46b1c05f7..93424b44e5 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java @@ -86,21 +86,22 @@ public class LwM2mServerListener { @Override public void cancelled(Observation observation) { - String msg = String.format("%s: Cancel Observation %s.", LOG_LW2M_INFO, observation.getPath()); + String msg = String.format("%s: Canceled Observation %s.", LOG_LW2M_INFO, observation.getPath()); service.sendLogsToThingsboard(msg, observation.getRegistrationId()); - log.trace(msg); + log.warn(msg); } @Override public void onResponse(Observation observation, Registration registration, ObserveResponse response) { if (registration != null) { - try { - service.onUpdateValueAfterReadResponse(registration, convertPathFromObjectIdToIdVer(observation.getPath().toString(), - registration), response, null); - } catch (Exception e) { - log.error("Observation/Read onResponse", e); - - } +// if (observation.getPath().isResource() || observation.getPath().isResourceInstance()) { +// String msg = String.format("%s: Successful Observation %s.", LOG_LW2M_INFO, +// observation.getPath()); +// log.warn(msg); +// service.sendLogsToThingsboard(msg, registration.getId()); +// } + service.onUpdateValueAfterReadResponse(registration, convertPathFromObjectIdToIdVer(observation.getPath().toString(), + registration), response, null); } } @@ -113,9 +114,8 @@ public class LwM2mServerListener { public void newObservation(Observation observation, Registration registration) { String msg = String.format("%s: Successful start newObservation %s.", LOG_LW2M_INFO, observation.getPath()); + log.warn(msg); service.sendLogsToThingsboard(msg, registration.getId()); - log.trace(msg); } }; - } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportMsgHandler.java index bb0be3aa36..794df65db5 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportMsgHandler.java @@ -21,6 +21,7 @@ import org.eclipse.leshan.server.registration.Registration; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.server.client.Lwm2mClientRpcRequest; import java.util.Collection; @@ -36,7 +37,7 @@ public interface LwM2mTransportMsgHandler { void onSleepingDev(Registration registration); - void setCancelObservations(Registration registration); + void setCancelObservationsAll(Registration registration); void onUpdateValueAfterReadResponse(Registration registration, String path, ReadResponse response, Lwm2mClientRpcRequest rpcRequest); @@ -63,4 +64,6 @@ public interface LwM2mTransportMsgHandler { void onAwakeDev(Registration registration); void sendLogsToThingsboard(String msg, String registrationId); + + LwM2MTransportServerConfig getConfig(); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java index a187143974..c5290b5eb6 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java @@ -19,6 +19,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Response; +import org.eclipse.leshan.core.Link; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; @@ -35,7 +36,6 @@ import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.core.request.ReadRequest; import org.eclipse.leshan.core.request.WriteRequest; import org.eclipse.leshan.core.request.exception.ClientSleepingException; -import org.eclipse.leshan.core.response.CancelObservationResponse; import org.eclipse.leshan.core.response.DeleteResponse; import org.eclipse.leshan.core.response.DiscoverResponse; import org.eclipse.leshan.core.response.ExecuteResponse; @@ -68,15 +68,26 @@ import java.util.stream.Collectors; import static org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT; import static org.eclipse.leshan.core.ResponseCode.BAD_REQUEST; import static org.eclipse.leshan.core.ResponseCode.NOT_FOUND; +import static org.thingsboard.server.common.data.firmware.FirmwareUpdateStatus.DOWNLOADED; +import static org.thingsboard.server.common.data.firmware.FirmwareUpdateStatus.FAILED; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper.getContentFormatByResourceModelType; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.DEFAULT_TIMEOUT; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FR_PATH_RESOURCE_VER_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_PACKAGE_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_UPDATE_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_ERROR; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_INFO; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_VALUE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.DISCOVER; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.DISCOVER_All; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.EXECUTE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.OBSERVE_CANCEL; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.OBSERVE_READ_ALL; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.RESPONSE_CHANNEL; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_REPLACE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_UPDATE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.RESPONSE_REQUEST_CHANNEL; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_INSTALL_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_PACKAGE_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromIdVerToObjectId; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromObjectIdToIdVer; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.createWriteAttributeRequest; @@ -86,20 +97,20 @@ import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.c @TbLwM2mTransportComponent @RequiredArgsConstructor public class LwM2mTransportRequest { - private ExecutorService executorResponse; + private ExecutorService responseRequestExecutor; public LwM2mValueConverterImpl converter; private final LwM2mTransportContext context; private final LwM2MTransportServerConfig config; private final LwM2mClientContext lwM2mClientContext; - private final DefaultLwM2MTransportMsgHandler serviceImpl; + private final DefaultLwM2MTransportMsgHandler handler; @PostConstruct public void init() { this.converter = LwM2mValueConverterImpl.getInstance(); - executorResponse = Executors.newFixedThreadPool(this.config.getResponsePoolSize(), - new NamedThreadFactory(String.format("LwM2M %s channel response", RESPONSE_CHANNEL))); + responseRequestExecutor = Executors.newFixedThreadPool(this.config.getResponsePoolSize(), + new NamedThreadFactory(String.format("LwM2M %s channel response after request", RESPONSE_REQUEST_CHANNEL))); } /** @@ -115,154 +126,186 @@ public class LwM2mTransportRequest { String contentFormatName, Object params, long timeoutInMs, Lwm2mClientRpcRequest rpcRequest) { try { String target = convertPathFromIdVerToObjectId(targetIdVer); - DownlinkRequest request = null; ContentFormat contentFormat = contentFormatName != null ? ContentFormat.fromName(contentFormatName.toUpperCase()) : ContentFormat.DEFAULT; LwM2mClient lwM2MClient = this.lwM2mClientContext.getOrRegister(registration); LwM2mPath resultIds = target != null ? new LwM2mPath(target) : null; if (!OBSERVE_READ_ALL.name().equals(typeOper.name()) && resultIds != null && registration != null && resultIds.getObjectId() >= 0 && lwM2MClient != null) { if (lwM2MClient.isValidObjectVersion(targetIdVer)) { timeoutInMs = timeoutInMs > 0 ? timeoutInMs : DEFAULT_TIMEOUT; - ResourceModel resourceModel = null; - switch (typeOper) { - case READ: - request = new ReadRequest(contentFormat, target); - break; - case DISCOVER: - request = new DiscoverRequest(target); - break; - case OBSERVE: - if (resultIds.isResource()) { - request = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId()); - } else if (resultIds.isObjectInstance()) { - request = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId()); - } else if (resultIds.getObjectId() >= 0) { - request = new ObserveRequest(contentFormat, resultIds.getObjectId()); - } - break; - case OBSERVE_CANCEL: - /* - lwM2MTransportRequest.sendAllRequest(lwServer, registration, path, POST_TYPE_OPER_OBSERVE_CANCEL, null, null, null, null, context.getTimeout()); - At server side this will not remove the observation from the observation store, to do it you need to use - {@code ObservationService#cancelObservation()} - */ - context.getServer().getObservationService().cancelObservations(registration, target); - break; - case EXECUTE: - resourceModel = lwM2MClient.getResourceModel(targetIdVer, this.config - .getModelProvider()); - if (params != null && !resourceModel.multiple) { - request = new ExecuteRequest(target, (String) this.converter.convertValue(params, resourceModel.type, ResourceModel.Type.STRING, resultIds)); - } else { - request = new ExecuteRequest(target); - } - break; - case WRITE_REPLACE: - // Request to write a String Single-Instance Resource using the TLV content format. - resourceModel = lwM2MClient.getResourceModel(targetIdVer, this.config.getModelProvider()); - if (contentFormat.equals(ContentFormat.TLV)) { - request = this.getWriteRequestSingleResource(null, resultIds.getObjectId(), - resultIds.getObjectInstanceId(), resultIds.getResourceId(), params, resourceModel.type, - registration, rpcRequest); - } - // Mode.REPLACE && Request to write a String Single-Instance Resource using the given content format (TEXT, TLV, JSON) - else if (!contentFormat.equals(ContentFormat.TLV)) { - request = this.getWriteRequestSingleResource(contentFormat, resultIds.getObjectId(), - resultIds.getObjectInstanceId(), resultIds.getResourceId(), params, resourceModel.type, - registration, rpcRequest); - } - break; - case WRITE_UPDATE: - if (resultIds.isResource()) { - /** - * send request: path = '/3/0' node == wM2mObjectInstance - * with params == "\"resources\": {15: resource:{id:15. value:'+01'...}} - **/ - Collection resources = lwM2MClient.getNewOneResourceForInstance( - targetIdVer, params, - this.config.getModelProvider(), - this.converter); - request = new WriteRequest(WriteRequest.Mode.UPDATE, contentFormat, resultIds.getObjectId(), - resultIds.getObjectInstanceId(), resources); - } - - /** - * params = "{\"id\":0,\"resources\":[{\"id\":14,\"value\":\"+5\"},{\"id\":15,\"value\":\"+9\"}]}" - * - * int rscId = resultIds.getObjectInstanceId(); - */ - - else if (resultIds.isObjectInstance()) { - if (((ConcurrentHashMap) params).size() > 0) { - Collection resources = lwM2MClient.getNewManyResourcesForInstance( - targetIdVer, params, - this.config.getModelProvider(), - this.converter); - request = new WriteRequest(WriteRequest.Mode.UPDATE, contentFormat, resultIds.getObjectId(), - resultIds.getObjectInstanceId(), resources); - } - } else if (resultIds.getObjectId() >= 0) { - request = new ObserveRequest(resultIds.getObjectId()); - } - break; - case WRITE_ATTRIBUTES: - request = createWriteAttributeRequest(target, params); - break; - case DELETE: - request = new DeleteRequest(target); - break; - } - + DownlinkRequest request = createRequest (registration, lwM2MClient, typeOper, contentFormat, target, + targetIdVer, resultIds, params, rpcRequest); if (request != null) { try { this.sendRequest(registration, lwM2MClient, request, timeoutInMs, rpcRequest); } catch (ClientSleepingException e) { DownlinkRequest finalRequest = request; long finalTimeoutInMs = timeoutInMs; - lwM2MClient.getQueuedRequests().add(() -> sendRequest(registration, lwM2MClient, finalRequest, finalTimeoutInMs, rpcRequest)); + Lwm2mClientRpcRequest finalRpcRequest = rpcRequest; + lwM2MClient.getQueuedRequests().add(() -> sendRequest(registration, lwM2MClient, finalRequest, finalTimeoutInMs, finalRpcRequest)); } catch (Exception e) { log.error("[{}] [{}] [{}] Failed to send downlink.", registration.getEndpoint(), targetIdVer, typeOper.name(), e); } - } else if (OBSERVE_CANCEL == typeOper) { - log.trace("[{}], [{}] - [{}] SendRequest", registration.getEndpoint(), typeOper.name(), targetIdVer); - if (rpcRequest != null) { - rpcRequest.setInfoMsg(null); - serviceImpl.sentRpcRequest(rpcRequest, CONTENT.name(), null, null); + } + else if (WRITE_UPDATE.name().equals(typeOper.name())) { + Lwm2mClientRpcRequest rpcRequestClone = (Lwm2mClientRpcRequest) rpcRequest.clone(); + if (rpcRequestClone != null) { + String errorMsg = String.format("Path %s params is not valid", targetIdVer); + handler.sentRpcRequest(rpcRequestClone, BAD_REQUEST.getName(), errorMsg, LOG_LW2M_ERROR); + rpcRequest = null; } - } else { + } + else if (!OBSERVE_CANCEL.name().equals(typeOper.name())) { log.error("[{}], [{}] - [{}] error SendRequest", registration.getEndpoint(), typeOper.name(), targetIdVer); if (rpcRequest != null) { + ResourceModel resourceModel = lwM2MClient.getResourceModel(targetIdVer, this.config.getModelProvider()); String errorMsg = resourceModel == null ? String.format("Path %s not found in object version", targetIdVer) : "SendRequest - null"; - serviceImpl.sentRpcRequest(rpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR); + handler.sentRpcRequest(rpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR); } } } else if (rpcRequest != null) { String errorMsg = String.format("Path %s not found in object version", targetIdVer); - serviceImpl.sentRpcRequest(rpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR); + handler.sentRpcRequest(rpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR); + } + } else if (OBSERVE_READ_ALL.name().equals(typeOper.name()) || DISCOVER_All.name().equals(typeOper.name())) { + Set paths; + if (OBSERVE_READ_ALL.name().equals(typeOper.name())) { + Set observations = context.getServer().getObservationService().getObservations(registration); + paths = observations.stream().map(observation -> observation.getPath().toString()).collect(Collectors.toUnmodifiableSet()); + } else { + assert registration != null; + Link[] objectLinks = registration.getSortedObjectLinks(); + paths = Arrays.stream(objectLinks).map(Link::toString).collect(Collectors.toUnmodifiableSet()); + String msg = String.format("%s: type operation %s paths - %s", LOG_LW2M_INFO, + typeOper.name(), paths); + handler.sendLogsToThingsboard(msg, registration.getId()); } - } else if (OBSERVE_READ_ALL.name().equals(typeOper.name())) { - Set observations = context.getServer().getObservationService().getObservations(registration); - Set observationPaths = observations.stream().map(observation -> observation.getPath().toString()).collect(Collectors.toUnmodifiableSet()); - String msg = String.format("%s: type operation %s observation paths - %s", LOG_LW2M_INFO, - OBSERVE_READ_ALL.type, observationPaths); - serviceImpl.sendLogsToThingsboard(msg, registration.getId()); - log.trace("[{}] [{}], [{}]", typeOper.name(), registration.getEndpoint(), msg); if (rpcRequest != null) { - String valueMsg = String.format("Observation paths - %s", observationPaths); - serviceImpl.sentRpcRequest(rpcRequest, CONTENT.name(), valueMsg, LOG_LW2M_VALUE); + String valueMsg = String.format("Paths - %s", paths); + handler.sentRpcRequest(rpcRequest, CONTENT.name(), valueMsg, LOG_LW2M_VALUE); } + } else if (OBSERVE_CANCEL.name().equals(typeOper.name())) { + int observeCancelCnt = context.getServer().getObservationService().cancelObservations(registration); + String observeCancelMsgAll = String.format("%s: type operation %s paths: All count: %d", LOG_LW2M_INFO, + OBSERVE_CANCEL.name(), observeCancelCnt); + this.afterObserveCancel(registration, observeCancelCnt, observeCancelMsgAll, rpcRequest); } } catch (Exception e) { String msg = String.format("%s: type operation %s %s", LOG_LW2M_ERROR, typeOper.name(), e.getMessage()); - serviceImpl.sendLogsToThingsboard(msg, registration.getId()); - try { - throw new Exception(e); - } catch (Exception exception) { - exception.printStackTrace(); + handler.sendLogsToThingsboard(msg, registration.getId()); + if (rpcRequest != null) { + String errorMsg = String.format("Path %s type operation %s %s", targetIdVer, typeOper.name(), e.getMessage()); + handler.sentRpcRequest(rpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR); } } } + private DownlinkRequest createRequest (Registration registration, LwM2mClient lwM2MClient, LwM2mTypeOper typeOper, + ContentFormat contentFormat, String target, String targetIdVer, + LwM2mPath resultIds, Object params, Lwm2mClientRpcRequest rpcRequest) { + DownlinkRequest request = null; + switch (typeOper) { + case READ: + request = new ReadRequest(contentFormat, target); + break; + case DISCOVER: + request = new DiscoverRequest(target); + break; + case OBSERVE: + String msg = String.format("%s: Send Observation %s.", LOG_LW2M_INFO, targetIdVer); + log.warn(msg); + if (resultIds.isResource()) { + Set observations = context.getServer().getObservationService().getObservations(registration); + Set paths = observations.stream().filter(observation -> observation.getPath().equals(resultIds)).collect(Collectors.toSet()); + if (paths.size() == 0) { + request = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId()); + } else { + request = new ReadRequest(contentFormat, target); + } + } else if (resultIds.isObjectInstance()) { + request = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId()); + } else if (resultIds.getObjectId() >= 0) { + request = new ObserveRequest(contentFormat, resultIds.getObjectId()); + } + break; + case OBSERVE_CANCEL: + /* + lwM2MTransportRequest.sendAllRequest(lwServer, registration, path, POST_TYPE_OPER_OBSERVE_CANCEL, null, null, null, null, context.getTimeout()); + At server side this will not remove the observation from the observation store, to do it you need to use + {@code ObservationService#cancelObservation()} + */ + int observeCancelCnt = context.getServer().getObservationService().cancelObservations(registration, target); + String observeCancelMsg = String.format("%s: type operation %s paths: %s count: %d", LOG_LW2M_INFO, + OBSERVE_CANCEL.name(), target, observeCancelCnt); + this.afterObserveCancel(registration, observeCancelCnt, observeCancelMsg, rpcRequest); + break; + case EXECUTE: + ResourceModel resourceModelExe = lwM2MClient.getResourceModel(targetIdVer, this.config.getModelProvider()); + if (params != null && !resourceModelExe.multiple) { + request = new ExecuteRequest(target, (String) this.converter.convertValue(params, resourceModelExe.type, ResourceModel.Type.STRING, resultIds)); + } else { + request = new ExecuteRequest(target); + } + break; + case WRITE_REPLACE: + /** + * Request to write a String Single-Instance Resource using the TLV content format. + * Type from resourceModel -> STRING, INTEGER, FLOAT, BOOLEAN, OPAQUE, TIME, OBJLNK + * contentFormat -> TLV, TLV, TLV, TLV, OPAQUE, TLV, LINK + * JSON, TEXT; + **/ + ResourceModel resourceModelWrite = lwM2MClient.getResourceModel(targetIdVer, this.config.getModelProvider()); + contentFormat = getContentFormatByResourceModelType(resourceModelWrite, contentFormat); + request = this.getWriteRequestSingleResource(contentFormat, resultIds.getObjectId(), + resultIds.getObjectInstanceId(), resultIds.getResourceId(), params, resourceModelWrite.type, + registration, rpcRequest); + break; + case WRITE_UPDATE: + if (resultIds.isResource()) { + /** + * send request: path = '/3/0' node == wM2mObjectInstance + * with params == "\"resources\": {15: resource:{id:15. value:'+01'...}} + **/ + Collection resources = lwM2MClient.getNewResourceForInstance( + targetIdVer, params, + this.config.getModelProvider(), + this.converter); + contentFormat = getContentFormatByResourceModelType(lwM2MClient.getResourceModel(targetIdVer, this.config.getModelProvider()), + contentFormat); + request = new WriteRequest(WriteRequest.Mode.UPDATE, contentFormat, resultIds.getObjectId(), + resultIds.getObjectInstanceId(), resources); + } + /** + * params = "{\"id\":0,\"resources\":[{\"id\":14,\"value\":\"+5\"},{\"id\":15,\"value\":\"+9\"}]}" + * int rscId = resultIds.getObjectInstanceId(); + * contentFormat – Format of the payload (TLV or JSON). + */ + else if (resultIds.isObjectInstance()) { + if (((ConcurrentHashMap) params).size() > 0) { + Collection resources = lwM2MClient.getNewResourcesForInstance( + targetIdVer, params, + this.config.getModelProvider(), + this.converter); + if (resources.size() > 0) { + contentFormat = contentFormat.equals(ContentFormat.JSON) ? contentFormat : ContentFormat.TLV; + request = new WriteRequest(WriteRequest.Mode.UPDATE, contentFormat, resultIds.getObjectId(), + resultIds.getObjectInstanceId(), resources); + } + } + } else if (resultIds.getObjectId() >= 0) { + request = new ObserveRequest(resultIds.getObjectId()); + } + break; + case WRITE_ATTRIBUTES: + request = createWriteAttributeRequest(target, params); + break; + case DELETE: + request = new DeleteRequest(target); + break; + } + return request; + } + /** * @param registration - * @param request - @@ -273,52 +316,66 @@ public class LwM2mTransportRequest { private void sendRequest(Registration registration, LwM2mClient lwM2MClient, DownlinkRequest request, long timeoutInMs, Lwm2mClientRpcRequest rpcRequest) { context.getServer().send(registration, request, timeoutInMs, (ResponseCallback) response -> { + if (!lwM2MClient.isInit()) { - lwM2MClient.initReadValue(this.serviceImpl, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); + lwM2MClient.initReadValue(this.handler, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); } if (CoAP.ResponseCode.isSuccess(((Response) response.getCoapResponse()).getCode())) { this.handleResponse(registration, request.getPath().toString(), response, request, rpcRequest); } else { String msg = String.format("%s: SendRequest %s: CoapCode - %s Lwm2m code - %d name - %s Resource path - %s", LOG_LW2M_ERROR, request.getClass().getName().toString(), ((Response) response.getCoapResponse()).getCode(), response.getCode().getCode(), response.getCode().getName(), request.getPath().toString()); - serviceImpl.sendLogsToThingsboard(msg, registration.getId()); + handler.sendLogsToThingsboard(msg, registration.getId()); log.error("[{}] [{}], [{}] - [{}] [{}] error SendRequest", request.getClass().getName().toString(), registration.getEndpoint(), ((Response) response.getCoapResponse()).getCode(), response.getCode(), request.getPath().toString()); if (!lwM2MClient.isInit()) { - lwM2MClient.initReadValue(this.serviceImpl, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); + lwM2MClient.initReadValue(this.handler, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); } + /** Not Found */ if (rpcRequest != null) { - serviceImpl.sentRpcRequest(rpcRequest, response.getCode().getName(), response.getErrorMessage(), LOG_LW2M_ERROR); + handler.sentRpcRequest(rpcRequest, response.getCode().getName(), response.getErrorMessage(), LOG_LW2M_ERROR); } - /* Not Found - set setClient_fw_version = empty - */ - if (FR_PATH_RESOURCE_VER_ID.equals(request.getPath().toString()) && lwM2MClient.isUpdateFw()) { - lwM2MClient.setUpdateFw(false); - lwM2MClient.getFrUpdate().setClientFwVersion(""); - log.warn("updateFirmwareClient1"); - serviceImpl.updateFirmwareClient(lwM2MClient); + /** Not Found + set setClient_fw_info... = empty + **/ + if (lwM2MClient.getFwUpdate().isInfoFwSwUpdate()) { + lwM2MClient.getFwUpdate().initReadValue(handler, this, request.getPath().toString()); + } + if (lwM2MClient.getSwUpdate().isInfoFwSwUpdate()) { + lwM2MClient.getSwUpdate().initReadValue(handler, this, request.getPath().toString()); + } + if (request.getPath().toString().equals(FW_PACKAGE_ID) || request.getPath().toString().equals(SW_PACKAGE_ID)) { + this.afterWriteFwSWUpdateError(registration, request, response.getErrorMessage()); + } + if (request.getPath().toString().equals(FW_UPDATE_ID) || request.getPath().toString().equals(SW_INSTALL_ID)) { + this.afterExecuteFwSwUpdateError(registration, request, response.getErrorMessage()); } } }, e -> { - /* version == null - set setClient_fw_version = empty - */ - if (FR_PATH_RESOURCE_VER_ID.equals(request.getPath().toString()) && lwM2MClient.isUpdateFw()) { - lwM2MClient.setUpdateFw(false); - lwM2MClient.getFrUpdate().setClientFwVersion(""); - log.warn("updateFirmwareClient2"); - serviceImpl.updateFirmwareClient(lwM2MClient); + /** version == null + set setClient_fw_info... = empty + **/ + if (lwM2MClient.getFwUpdate().isInfoFwSwUpdate()) { + lwM2MClient.getFwUpdate().initReadValue(handler, this, request.getPath().toString()); + } + if (lwM2MClient.getSwUpdate().isInfoFwSwUpdate()) { + lwM2MClient.getSwUpdate().initReadValue(handler, this, request.getPath().toString()); + } + if (request.getPath().toString().equals(FW_PACKAGE_ID) || request.getPath().toString().equals(SW_PACKAGE_ID)) { + this.afterWriteFwSWUpdateError(registration, request, e.getMessage()); + } + if (request.getPath().toString().equals(FW_UPDATE_ID) || request.getPath().toString().equals(SW_INSTALL_ID)) { + this.afterExecuteFwSwUpdateError(registration, request, e.getMessage()); } if (!lwM2MClient.isInit()) { - lwM2MClient.initReadValue(this.serviceImpl, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); + lwM2MClient.initReadValue(this.handler, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); } String msg = String.format("%s: SendRequest %s: Resource path - %s msg error - %s", LOG_LW2M_ERROR, request.getClass().getName().toString(), request.getPath().toString(), e.getMessage()); - serviceImpl.sendLogsToThingsboard(msg, registration.getId()); + handler.sendLogsToThingsboard(msg, registration.getId()); log.error("[{}] [{}] - [{}] error SendRequest", request.getClass().getName().toString(), request.getPath().toString(), e.toString()); if (rpcRequest != null) { - serviceImpl.sentRpcRequest(rpcRequest, CoAP.CodeClass.ERROR_RESPONSE.name(), e.getMessage(), LOG_LW2M_ERROR); + handler.sentRpcRequest(rpcRequest, CoAP.CodeClass.ERROR_RESPONSE.name(), e.getMessage(), LOG_LW2M_ERROR); } }); } @@ -360,11 +417,11 @@ public class LwM2mTransportRequest { String patn = "/" + objectId + "/" + instanceId + "/" + resourceId; String msg = String.format(LOG_LW2M_ERROR + ": NumberFormatException: Resource path - %s type - %s value - %s msg error - %s SendRequest to Client", patn, type, value, e.toString()); - serviceImpl.sendLogsToThingsboard(msg, registration.getId()); + handler.sendLogsToThingsboard(msg, registration.getId()); log.error("Path: [{}] type: [{}] value: [{}] errorMsg: [{}]]", patn, type, value, e.toString()); if (rpcRequest != null) { String errorMsg = String.format("NumberFormatException: Resource path - %s type - %s value - %s", patn, type, value); - serviceImpl.sentRpcRequest(rpcRequest, BAD_REQUEST.getName(), errorMsg, LOG_LW2M_ERROR); + handler.sentRpcRequest(rpcRequest, BAD_REQUEST.getName(), errorMsg, LOG_LW2M_ERROR); } return null; } @@ -372,7 +429,7 @@ public class LwM2mTransportRequest { private void handleResponse(Registration registration, final String path, LwM2mResponse response, DownlinkRequest request, Lwm2mClientRpcRequest rpcRequest) { - executorResponse.submit(() -> { + responseRequestExecutor.submit(() -> { try { this.sendResponse(registration, path, response, request, rpcRequest); } catch (Exception e) { @@ -391,39 +448,37 @@ public class LwM2mTransportRequest { private void sendResponse(Registration registration, String path, LwM2mResponse response, DownlinkRequest request, Lwm2mClientRpcRequest rpcRequest) { String pathIdVer = convertPathFromObjectIdToIdVer(path, registration); + String msgLog = ""; if (response instanceof ReadResponse) { - serviceImpl.onUpdateValueAfterReadResponse(registration, pathIdVer, (ReadResponse) response, rpcRequest); - } else if (response instanceof CancelObservationResponse) { - log.info("[{}] Path [{}] CancelObservationResponse 3_Send", pathIdVer, response); - + handler.onUpdateValueAfterReadResponse(registration, pathIdVer, (ReadResponse) response, rpcRequest); } else if (response instanceof DeleteResponse) { - log.info("[{}] Path [{}] DeleteResponse 5_Send", pathIdVer, response); + log.warn("[{}] Path [{}] DeleteResponse 5_Send", pathIdVer, response); } else if (response instanceof DiscoverResponse) { - log.info("[{}] [{}] - [{}] [{}] Discovery value: [{}]", registration.getEndpoint(), - ((Response) response.getCoapResponse()).getCode(), response.getCode(), - request.getPath().toString(), ((DiscoverResponse) response).getObjectLinks()); + String discoverValue = Link.serialize(((DiscoverResponse)response).getObjectLinks()); + msgLog = String.format("%s: type operation: %s path: %s value: %s", + LOG_LW2M_INFO, DISCOVER.name(), request.getPath().toString(), discoverValue); + handler.sendLogsToThingsboard(msgLog, registration.getId()); + log.warn("DiscoverResponse: [{}]", (DiscoverResponse) response); if (rpcRequest != null) { - String discoveryMsg = String.format("%s", - Arrays.stream(((DiscoverResponse) response).getObjectLinks()).collect(Collectors.toSet())); - serviceImpl.sentRpcRequest(rpcRequest, response.getCode().getName(), discoveryMsg, LOG_LW2M_VALUE); + handler.sentRpcRequest(rpcRequest, response.getCode().getName(), discoverValue, LOG_LW2M_VALUE); } } else if (response instanceof ExecuteResponse) { - log.info("[{}] Path [{}] ExecuteResponse 7_Send", pathIdVer, response); + log.warn("[{}] Path [{}] ExecuteResponse 7_Send", pathIdVer, response); } else if (response instanceof WriteAttributesResponse) { - log.info("[{}] Path [{}] WriteAttributesResponse 8_Send", pathIdVer, response); + log.warn("[{}] Path [{}] WriteAttributesResponse 8_Send", pathIdVer, response); } else if (response instanceof WriteResponse) { - log.info("[{}] Path [{}] WriteResponse 9_Send", pathIdVer, response); + log.warn("[{}] Path [{}] WriteResponse 9_Send", pathIdVer, response); this.infoWriteResponse(registration, response, request); - serviceImpl.onWriteResponseOk(registration, pathIdVer, (WriteRequest) request); + handler.onWriteResponseOk(registration, pathIdVer, (WriteRequest) request); } if (rpcRequest != null) { if (response instanceof ExecuteResponse || response instanceof WriteAttributesResponse || response instanceof DeleteResponse) { rpcRequest.setInfoMsg(null); - serviceImpl.sentRpcRequest(rpcRequest, response.getCode().getName(), null, null); + handler.sentRpcRequest(rpcRequest, response.getCode().getName(), null, null); } else if (response instanceof WriteResponse) { - serviceImpl.sentRpcRequest(rpcRequest, response.getCode().getName(), null, LOG_LW2M_INFO); + handler.sentRpcRequest(rpcRequest, response.getCode().getName(), null, LOG_LW2M_INFO); } } } @@ -447,21 +502,73 @@ public class LwM2mTransportRequest { Math.min(valueLength, config.getLogMaxLength()))); } value = valueLength > config.getLogMaxLength() ? value + "..." : value; - msg = String.format("%s: Update finished successfully: Lwm2m code - %d Resource path - %s length - %s value - %s", + msg = String.format("%s: Update finished successfully: Lwm2m code - %d Resource path: %s length: %s value: %s", LOG_LW2M_INFO, response.getCode().getCode(), request.getPath().toString(), valueLength, value); } else { value = this.converter.convertValue(singleResource.getValue(), singleResource.getType(), ResourceModel.Type.STRING, request.getPath()); - msg = String.format("%s: Update finished successfully: Lwm2m code - %d Resource path - %s value - %s", + msg = String.format("%s: Update finished successfully. Lwm2m code: %d Resource path: %s value: %s", LOG_LW2M_INFO, response.getCode().getCode(), request.getPath().toString(), value); } if (msg != null) { - serviceImpl.sendLogsToThingsboard(msg, registration.getId()); - log.warn("[{}] [{}] [{}] - [{}] [{}] Update finished successfully: [{}]", request.getClass().getName(), registration.getEndpoint(), - ((Response) response.getCoapResponse()).getCode(), response.getCode(), request.getPath().toString(), value); + handler.sendLogsToThingsboard(msg, registration.getId()); + if (request.getPath().toString().equals(FW_PACKAGE_ID) || request.getPath().toString().equals(SW_PACKAGE_ID)) { + this.afterWriteSuccessFwSwUpdate(registration, request); + } } } catch (Exception e) { log.trace("Fail convert value from request to string. ", e); } } + + /** + * After finish operation FwSwUpdate Write (success): + * fw_state/sw_state = DOWNLOADED + * send operation Execute + */ + private void afterWriteSuccessFwSwUpdate(Registration registration, DownlinkRequest request) { + LwM2mClient lwM2MClient = this.lwM2mClientContext.getClientByRegistrationId(registration.getId()); + if (request.getPath().toString().equals(FW_PACKAGE_ID) && lwM2MClient.getFwUpdate() != null) { + lwM2MClient.getFwUpdate().setStateUpdate(DOWNLOADED.name()); + lwM2MClient.getFwUpdate().sendLogs(handler, WRITE_REPLACE.name(), LOG_LW2M_INFO, null); + } + if (request.getPath().toString().equals(SW_PACKAGE_ID) && lwM2MClient.getSwUpdate() != null) { + lwM2MClient.getSwUpdate().setStateUpdate(DOWNLOADED.name()); + lwM2MClient.getSwUpdate().sendLogs(handler, WRITE_REPLACE.name(), LOG_LW2M_INFO, null); + } + } + + /** + * After finish operation FwSwUpdate Write (error): fw_state = FAILED + */ + private void afterWriteFwSWUpdateError(Registration registration, DownlinkRequest request, String msgError) { + LwM2mClient lwM2MClient = this.lwM2mClientContext.getClientByRegistrationId(registration.getId()); + if (request.getPath().toString().equals(FW_PACKAGE_ID) && lwM2MClient.getFwUpdate() != null) { + lwM2MClient.getFwUpdate().setStateUpdate(FAILED.name()); + lwM2MClient.getFwUpdate().sendLogs(handler, WRITE_REPLACE.name(), LOG_LW2M_ERROR, msgError); + } + if (request.getPath().toString().equals(SW_PACKAGE_ID) && lwM2MClient.getSwUpdate() != null) { + lwM2MClient.getSwUpdate().setStateUpdate(FAILED.name()); + lwM2MClient.getSwUpdate().sendLogs(handler, WRITE_REPLACE.name(), LOG_LW2M_ERROR, msgError); + } + } + + private void afterExecuteFwSwUpdateError(Registration registration, DownlinkRequest request, String msgError) { + LwM2mClient lwM2MClient = this.lwM2mClientContext.getClientByRegistrationId(registration.getId()); + if (request.getPath().toString().equals(FW_UPDATE_ID) && lwM2MClient.getFwUpdate() != null) { + lwM2MClient.getFwUpdate().sendLogs(handler, EXECUTE.name(), LOG_LW2M_ERROR, msgError); + } + if (request.getPath().toString().equals(SW_INSTALL_ID) && lwM2MClient.getSwUpdate() != null) { + lwM2MClient.getSwUpdate().sendLogs(handler, EXECUTE.name(), LOG_LW2M_ERROR, msgError); + } + } + + private void afterObserveCancel(Registration registration, int observeCancelCnt, String observeCancelMsg, Lwm2mClientRpcRequest rpcRequest) { + handler.sendLogsToThingsboard(observeCancelMsg, registration.getId()); + log.warn("[{}]", observeCancelMsg); + if (rpcRequest != null) { + rpcRequest.setInfoMsg(String.format("Count: %d", observeCancelCnt)); + handler.sentRpcRequest(rpcRequest, CONTENT.name(), null, LOG_LW2M_INFO); + } + } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java index 789faecb75..fff35ea855 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java @@ -38,6 +38,7 @@ import org.eclipse.leshan.core.model.InvalidDDFFileException; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.codec.CodecException; +import org.eclipse.leshan.core.request.ContentFormat; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; @@ -54,7 +55,6 @@ import java.util.ArrayList; import java.util.List; import static org.thingsboard.server.gen.transport.TransportProtos.KeyValueType.BOOLEAN_V; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_TELEMETRY; @Slf4j @Component @@ -69,7 +69,7 @@ public class LwM2mTransportServerHelper { * send to Thingsboard Attribute || Telemetry * * @param msg - JsonObject: [{name: value}] - * @return - dummy + * @return - dummyWriteReplace {\"targetIdVer\":\"/19_1.0/0/0\",\"value\":0082} */ private TransportServiceCallback getPubAckCallbackSendAttrTelemetry(final T msg) { return new TransportServiceCallback<>() { @@ -136,16 +136,16 @@ public class LwM2mTransportServerHelper { } /** - * - * @param logMsg - info about Logs + * @param value - info about Logs * @return- KeyValueProto for telemetry (Logs) */ - public List getKvLogyToThingsboard(String logMsg) { + public List getKvStringtoThingsboard(String key, String value) { List result = new ArrayList<>(); + value = value.replaceAll("<", "").replaceAll(">", ""); result.add(TransportProtos.KeyValueProto.newBuilder() - .setKey(LOG_LW2M_TELEMETRY) + .setKey(key) .setType(TransportProtos.KeyValueType.STRING_V) - .setStringV(logMsg).build()); + .setStringV(value).build()); return result; } @@ -181,8 +181,7 @@ public class LwM2mTransportServerHelper { } /** - * - * @param currentType - + * @param currentType - * @param resourcePath - * @return */ @@ -204,6 +203,28 @@ public class LwM2mTransportServerHelper { throw new CodecException("Invalid ResourceModel_Type for resource %s, got %s", resourcePath, currentType); } + public static ContentFormat convertResourceModelTypeToContentFormat(ResourceModel.Type type) { + switch (type) { + case BOOLEAN: + case STRING: + case TIME: + case INTEGER: + case FLOAT: + return ContentFormat.TLV; + case OPAQUE: + return ContentFormat.OPAQUE; + case OBJLNK: + return ContentFormat.LINK; + default: + } + throw new CodecException("Invalid ResourceModel_Type for %s ContentFormat.", type); + } + + public static ContentFormat getContentFormatByResourceModelType(ResourceModel resourceModel, ContentFormat contentFormat) { + return contentFormat.equals(ContentFormat.TLV) ? convertResourceModelTypeToContentFormat(resourceModel.type) : + contentFormat; + } + public static Object getValueFromKvProto(TransportProtos.KeyValueProto kv) { switch (kv.getType()) { case BOOLEAN_V: diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportUtil.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportUtil.java index eddc5835c3..c84b4a52b2 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportUtil.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportUtil.java @@ -43,6 +43,7 @@ import org.eclipse.leshan.server.registration.Registration; import org.nustaq.serialization.FSTConfiguration; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.firmware.FirmwareUpdateStatus; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; @@ -70,6 +71,12 @@ import static org.eclipse.leshan.core.model.ResourceModel.Type.OBJLNK; import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; import static org.eclipse.leshan.core.model.ResourceModel.Type.STRING; import static org.eclipse.leshan.core.model.ResourceModel.Type.TIME; +import static org.thingsboard.server.common.data.firmware.FirmwareUpdateStatus.DOWNLOADED; +import static org.thingsboard.server.common.data.firmware.FirmwareUpdateStatus.DOWNLOADING; +import static org.thingsboard.server.common.data.firmware.FirmwareUpdateStatus.FAILED; +import static org.thingsboard.server.common.data.firmware.FirmwareUpdateStatus.UPDATED; +import static org.thingsboard.server.common.data.firmware.FirmwareUpdateStatus.UPDATING; +import static org.thingsboard.server.common.data.firmware.FirmwareUpdateStatus.VERIFIED; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; @@ -97,7 +104,8 @@ public class LwM2mTransportUtil { public static final long DEFAULT_TIMEOUT = 2 * 60 * 1000L; // 2min in ms - public static final String LOG_LW2M_TELEMETRY = "logLwm2m"; + public static final String + LOG_LW2M_TELEMETRY = "LwM2MLog"; public static final String LOG_LW2M_INFO = "info"; public static final String LOG_LW2M_ERROR = "error"; public static final String LOG_LW2M_WARN = "warn"; @@ -107,11 +115,41 @@ public class LwM2mTransportUtil { public static final int LWM2M_STRATEGY_2 = 2; public static final String CLIENT_NOT_AUTHORIZED = "Client not authorized"; - - public static final Integer FR_OBJECT_ID = 5; - public static final Integer FR_RESOURCE_VER_ID = 7; - public static final String FR_PATH_RESOURCE_VER_ID = LWM2M_SEPARATOR_PATH + FR_OBJECT_ID + LWM2M_SEPARATOR_PATH - + "0" + LWM2M_SEPARATOR_PATH + FR_RESOURCE_VER_ID; + public static final String LWM2M_VERSION_DEFAULT = "1.0"; + + // FirmWare + public static final String FW_UPDATE = "Firmware update"; + public static final Integer FW_ID = 5; + // Package W + public static final String FW_PACKAGE_ID = "/5/0/0"; + // State R + public static final String FW_STATE_ID = "/5/0/3"; + // Update Result R + public static final String FW_RESULT_ID = "/5/0/5"; + // PkgName R + public static final String FW_NAME_ID = "/5/0/6"; + // PkgVersion R + public static final String FW_VER_ID = "/5/0/7"; + // Update E + public static final String FW_UPDATE_ID = "/5/0/2"; + + // SoftWare + public static final String SW_UPDATE = "Software update"; + public static final Integer SW_ID = 9; + // Package W + public static final String SW_PACKAGE_ID = "/9/0/2"; + // Update State R + public static final String SW_UPDATE_STATE_ID = "/9/0/7"; + // Update Result R + public static final String SW_RESULT_ID = "/9/0/9"; + // PkgName R + public static final String SW_NAME_ID = "/9/0/0"; + // PkgVersion R + public static final String SW_VER_ID = "/9/0/1"; + // Install E + public static final String SW_INSTALL_ID = "/9/0/4"; + // Uninstall E + public static final String SW_UN_INSTALL_ID = "/9/0/6"; public enum LwM2mTypeServer { BOOTSTRAP(0, "bootstrap"), @@ -144,19 +182,20 @@ public class LwM2mTransportUtil { */ READ(0, "Read"), DISCOVER(1, "Discover"), - OBSERVE_READ_ALL(2, "ObserveReadAll"), + DISCOVER_All(2, "DiscoverAll"), + OBSERVE_READ_ALL(3, "ObserveReadAll"), /** * POST */ - OBSERVE(3, "Observe"), - OBSERVE_CANCEL(4, "ObserveCancel"), - EXECUTE(5, "Execute"), + OBSERVE(4, "Observe"), + OBSERVE_CANCEL(5, "ObserveCancel"), + EXECUTE(6, "Execute"), /** * Replaces the Object Instance or the Resource(s) with the new value provided in the “Write” operation. (see * section 5.3.3 of the LW M2M spec). * if all resources are to be replaced */ - WRITE_REPLACE(6, "WriteReplace"), + WRITE_REPLACE(7, "WriteReplace"), /* PUT */ @@ -165,11 +204,18 @@ public class LwM2mTransportUtil { * 5.3.3 of the LW M2M spec). * if this is a partial update request */ - WRITE_UPDATE(7, "WriteUpdate"), - WRITE_ATTRIBUTES(8, "WriteAttributes"), - DELETE(9, "Delete"); - -// READ_INFO_FW(10, "ReadInfoFirmware"); + WRITE_UPDATE(8, "WriteUpdate"), + WRITE_ATTRIBUTES(9, "WriteAttributes"), + DELETE(10, "Delete"), + + // only for RPC + FW_READ_INFO(11, "FirmwareReadInfo"), + FW_UPDATE(12, "FirmwareUpdate"), + FW_UPDATE_URL(14, "FirmwareUpdateUrl"), + SW_READ_INFO(15, "SoftwareReadInfo"), + SW_UPDATE(16, "SoftwareUpdate"), + SW_UPDATE_URL(17, "SoftwareUpdateUrl"), + SW_UNINSTALL(18, "SoftwareUninstall"); public int code; public String type; @@ -189,10 +235,290 @@ public class LwM2mTransportUtil { } } + /** + * /** State R + * 0: Idle (before downloading or after successful updating) + * 1: Downloading (The data sequence is on the way) + * 2: Downloaded + * 3: Updating + */ + public enum StateFw { + IDLE(0, "Idle"), + DOWNLOADING(1, "Downloading"), + DOWNLOADED(2, "Downloaded"), + UPDATING(3, "Updating"); + + public int code; + public String type; + + StateFw(int code, String type) { + this.code = code; + this.type = type; + } + + public static StateFw fromStateFwByType(String type) { + for (StateFw to : StateFw.values()) { + if (to.type.equals(type)) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported FW State type : %s", type)); + } + + public static StateFw fromStateFwByCode(int code) { + for (StateFw to : StateFw.values()) { + if (to.code == code) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported FW State code : %s", code)); + } + } + + /** + * FW Update Result + * 0: Initial value. Once the updating process is initiated (Download /Update), this Resource MUST be reset to Initial value. + * 1: Firmware updated successfully. + * 2: Not enough flash memory for the new firmware package. + * 3: Out of RAM during downloading process. + * 4: Connection lost during downloading process. + * 5: Integrity check failure for new downloaded package. + * 6: Unsupported package type. + * 7: Invalid URI. + * 8: Firmware update failed. + * 9: Unsupported protocol. + */ + public enum UpdateResultFw { + INITIAL(0, "Initial value", false), + UPDATE_SUCCESSFULLY(1, "Firmware updated successfully", false), + NOT_ENOUGH(2, "Not enough flash memory for the new firmware package", false), + OUT_OFF_MEMORY(3, "Out of RAM during downloading process", false), + CONNECTION_LOST(4, "Connection lost during downloading process", true), + INTEGRITY_CHECK_FAILURE(5, "Integrity check failure for new downloaded package", true), + UNSUPPORTED_TYPE(6, "Unsupported package type", false), + INVALID_URI(7, "Invalid URI", false), + UPDATE_FAILED(8, "Firmware update failed", false), + UNSUPPORTED_PROTOCOL(9, "Unsupported protocol", false); + + public int code; + public String type; + public boolean isAgain; + + UpdateResultFw(int code, String type, boolean isAgain) { + this.code = code; + this.type = type; + this.isAgain = isAgain; + } + + public static UpdateResultFw fromUpdateResultFwByType(String type) { + for (UpdateResultFw to : UpdateResultFw.values()) { + if (to.type.equals(type)) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported FW Update Result type : %s", type)); + } + + public static UpdateResultFw fromUpdateResultFwByCode(int code) { + for (UpdateResultFw to : UpdateResultFw.values()) { + if (to.code == code) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported FW Update Result code : %s", code)); + } + } + + /** + * FirmwareUpdateStatus { + * DOWNLOADING, DOWNLOADED, VERIFIED, UPDATING, UPDATED, FAILED + */ + public static FirmwareUpdateStatus EqualsFwSateToFirmwareUpdateStatus(StateFw stateFw, UpdateResultFw updateResultFw) { + switch (updateResultFw) { + case INITIAL: + switch (stateFw) { + case IDLE: + return VERIFIED; + case DOWNLOADING: + return DOWNLOADING; + case DOWNLOADED: + return DOWNLOADED; + case UPDATING: + return UPDATING; + } + case UPDATE_SUCCESSFULLY: + return UPDATED; + case NOT_ENOUGH: + case OUT_OFF_MEMORY: + case CONNECTION_LOST: + case INTEGRITY_CHECK_FAILURE: + case UNSUPPORTED_TYPE: + case INVALID_URI: + case UPDATE_FAILED: + case UNSUPPORTED_PROTOCOL: + return FAILED; + default: + throw new CodecException("Invalid value stateFw %s %s for FirmwareUpdateStatus.", stateFw.name(), updateResultFw.name()); + } + } + + /** + * SW Update State R + * 0: INITIAL Before downloading. (see 5.1.2.1) + * 1: DOWNLOAD STARTED The downloading process has started and is on-going. (see 5.1.2.2) + * 2: DOWNLOADED The package has been completely downloaded (see 5.1.2.3) + * 3: DELIVERED In that state, the package has been correctly downloaded and is ready to be installed. (see 5.1.2.4) + * If executing the Install Resource failed, the state remains at DELIVERED. + * If executing the Install Resource was successful, the state changes from DELIVERED to INSTALLED. + * After executing the UnInstall Resource, the state changes to INITIAL. + * 4: INSTALLED + */ + public enum UpdateStateSw { + INITIAL(0, "Initial"), + DOWNLOAD_STARTED(1, "DownloadStarted"), + DOWNLOADED(2, "Downloaded"), + DELIVERED(3, "Delivered"), + INSTALLED(4, "Installed"); + + public int code; + public String type; + + UpdateStateSw(int code, String type) { + this.code = code; + this.type = type; + } + + public static UpdateStateSw fromUpdateStateSwByType(String type) { + for (UpdateStateSw to : UpdateStateSw.values()) { + if (to.type.equals(type)) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported SW State type : %s", type)); + } + + public static UpdateStateSw fromUpdateStateSwByCode(int code) { + for (UpdateStateSw to : UpdateStateSw.values()) { + if (to.code == code) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported SW State type : %s", code)); + } + } + + /** + * SW Update Result + * Contains the result of downloading or installing/uninstalling the software + * 0: Initial value. + * - Prior to download any new package in the Device, Update Result MUST be reset to this initial value. + * - One side effect of executing the Uninstall resource is to reset Update Result to this initial value "0". + * 1: Downloading. + * - The package downloading process is on-going. + * 2: Software successfully installed. + * 3: Successfully Downloaded and package integrity verified + * (( 4-49, for expansion, of other scenarios)) + * ** Failed + * 50: Not enough storage for the new software package. + * 51: Out of memory during downloading process. + * 52: Connection lost during downloading process. + * 53: Package integrity check failure. + * 54: Unsupported package type. + * 56: Invalid URI + * 57: Device defined update error + * 58: Software installation failure + * 59: Uninstallation Failure during forUpdate(arg=0) + * 60-200 : (for expansion, selection to be in blocks depending on new introduction of features) + * This Resource MAY be reported by sending Observe operation. + */ + public enum UpdateResultSw { + INITIAL(0, "Initial value", false), + DOWNLOADING(1, "Downloading", false), + SUCCESSFULLY_INSTALLED(2, "Software successfully installed", false), + SUCCESSFULLY_DOWNLOADED_VERIFIED(3, "Successfully Downloaded and package integrity verified", false), + NOT_ENOUGH_STORAGE(50, "Not enough storage for the new software package", true), + OUT_OFF_MEMORY(51, "Out of memory during downloading process", true), + CONNECTION_LOST(52, "Connection lost during downloading process", false), + PACKAGE_CHECK_FAILURE(53, "Package integrity check failure.", false), + UNSUPPORTED_PACKAGE_TYPE(54, "Unsupported package type", false), + INVALID_URI(56, "Invalid URI", true), + UPDATE_ERROR(57, "Device defined update error", true), + INSTALL_FAILURE(58, "Software installation failure", true), + UN_INSTALL_FAILURE(59, "Uninstallation Failure during forUpdate(arg=0)", true); + + public int code; + public String type; + public boolean isAgain; + + UpdateResultSw(int code, String type, boolean isAgain) { + this.code = code; + this.type = type; + this.isAgain = isAgain; + } + + public static UpdateResultSw fromUpdateResultSwByType(String type) { + for (UpdateResultSw to : UpdateResultSw.values()) { + if (to.type.equals(type)) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported SW Update Result type : %s", type)); + } + + public static UpdateResultSw fromUpdateResultSwByCode(int code) { + for (UpdateResultSw to : UpdateResultSw.values()) { + if (to.code == code) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported SW Update Result code : %s", code)); + } + } + + /** + * FirmwareUpdateStatus { + * DOWNLOADING, DOWNLOADED, VERIFIED, UPDATING, UPDATED, FAILED + */ + public static FirmwareUpdateStatus EqualsSwSateToFirmwareUpdateStatus(UpdateStateSw updateStateSw, UpdateResultSw updateResultSw) { + switch (updateResultSw) { + case INITIAL: + switch (updateStateSw) { + case INITIAL: + case DOWNLOAD_STARTED: + return DOWNLOADING; + case DOWNLOADED: + return DOWNLOADED; + case DELIVERED: + return VERIFIED; + } + case DOWNLOADING: + return DOWNLOADING; + case SUCCESSFULLY_INSTALLED: + return UPDATED; + case SUCCESSFULLY_DOWNLOADED_VERIFIED: + return VERIFIED; + case NOT_ENOUGH_STORAGE: + case OUT_OFF_MEMORY: + case CONNECTION_LOST: + case PACKAGE_CHECK_FAILURE: + case UNSUPPORTED_PACKAGE_TYPE: + case INVALID_URI: + case UPDATE_ERROR: + case INSTALL_FAILURE: + case UN_INSTALL_FAILURE: + return FAILED; + default: + throw new CodecException("Invalid value stateFw %s %s for FirmwareUpdateStatus.", updateStateSw.name(), updateResultSw.name()); + } + } + public static final String EVENT_AWAKE = "AWAKE"; + public static final String RESPONSE_REQUEST_CHANNEL = "RESP_REQ"; public static final String RESPONSE_CHANNEL = "RESP"; + public static final String OBSERVE_CHANNEL = "OBSERVE"; - public static boolean equalsResourceValue(Object valueOld, Object valueNew, ResourceModel.Type type, LwM2mPath resourcePath) throws CodecException { + public static boolean equalsResourceValue(Object valueOld, Object valueNew, ResourceModel.Type type, LwM2mPath + resourcePath) throws CodecException { switch (type) { case BOOLEAN: case INTEGER: @@ -256,7 +582,7 @@ public class LwM2mTransportUtil { * "/3_1.0/0/9": {"pmax": 45}, "/3_1.2": {ver": "3_1.2"}} */ public static LwM2mClientProfile toLwM2MClientProfile(DeviceProfile deviceProfile) { - if (deviceProfile != null && ((Lwm2mDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration()).getProperties().size() > 0) { + if (((Lwm2mDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration()).getProperties().size() > 0) { Object profile = ((Lwm2mDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration()).getProperties(); try { ObjectMapper mapper = new ObjectMapper(); @@ -375,7 +701,8 @@ public class LwM2mTransportUtil { return StringUtils.join(linkedListOut, ""); } - public static TransportServiceCallback getAckCallback(LwM2mClient lwM2MClient, int requestId, String typeTopic) { + public static TransportServiceCallback getAckCallback(LwM2mClient lwM2MClient, + int requestId, String typeTopic) { return new TransportServiceCallback() { @Override public void onSuccess(Void dummy) { @@ -420,7 +747,8 @@ public class LwM2mTransportUtil { return null; } - public static String validPathIdVer(String pathIdVer, Registration registration) throws IllegalArgumentException { + public static String validPathIdVer(String pathIdVer, Registration registration) throws + IllegalArgumentException { if (!pathIdVer.contains(LWM2M_SEPARATOR_PATH)) { throw new IllegalArgumentException(String.format("Error:")); } else { @@ -436,6 +764,7 @@ public class LwM2mTransportUtil { public static String convertPathFromObjectIdToIdVer(String path, Registration registration) { String ver = registration.getSupportedObject().get(new LwM2mPath(path).getObjectId()); + ver = ver != null ? ver : LWM2M_VERSION_DEFAULT; try { String[] keyArray = path.split(LWM2M_SEPARATOR_PATH); if (keyArray.length > 1) { @@ -531,7 +860,7 @@ public class LwM2mTransportUtil { case "ObjectLink": return OBJLNK; default: - return null; + return null; } } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java index 800c518adc..2d981e0d4e 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java @@ -82,7 +82,7 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { if (objectModel != null) return objectModel.resources.get(resourceId); else - log.warn("TbResources (Object model) with id [{}/0/{}] not found on the server", objectId, resourceId); + log.trace("TbResources (Object model) with id [{}/0/{}] not found on the server", objectId, resourceId); return null; } catch (Exception e) { log.error("", e); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MJsonAdaptor.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MJsonAdaptor.java index 65a1e14a4c..9c192ae801 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MJsonAdaptor.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MJsonAdaptor.java @@ -64,7 +64,6 @@ public class LwM2MJsonAdaptor implements LwM2MTransportAdaptor { } return result.build(); } catch (RuntimeException e) { - log.warn("Failed to decode get attributes request", e); throw new AdaptorException(e); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index d20de6ebe4..d3f1fb4343 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -18,7 +18,11 @@ package org.thingsboard.server.transport.lwm2m.server.client; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.LwM2mMultipleResource; +import org.eclipse.leshan.core.node.LwM2mObject; +import org.eclipse.leshan.core.node.LwM2mObjectInstance; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mSingleResource; @@ -28,9 +32,9 @@ import org.eclipse.leshan.server.security.SecurityInfo; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.data.firmware.FirmwareType; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; import org.thingsboard.server.transport.lwm2m.server.DefaultLwM2MTransportMsgHandler; import org.thingsboard.server.transport.lwm2m.server.LwM2mQueuedRequest; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; @@ -47,11 +51,14 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; +import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; +import static org.eclipse.leshan.core.model.ResourceModel.Type.STRING; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.TRANSPORT_DEFAULT_LWM2M_VERSION; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromIdVerToObjectId; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.getVerFromPathIdVerOrId; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromObjectIdToIdVer; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.equalsResourceTypeGetSimpleName; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.getVerFromPathIdVerOrId; @Slf4j public class LwM2mClient implements Cloneable { @@ -75,25 +82,27 @@ public class LwM2mClient implements Cloneable { private UUID profileId; @Getter @Setter - private volatile LwM2mFirmwareUpdate frUpdate; + private volatile LwM2mFwSwUpdate fwUpdate; + @Getter + @Setter + private volatile LwM2mFwSwUpdate swUpdate; @Getter @Setter private Registration registration; private ValidateDeviceCredentialsResponse credentials; + @Getter private final Map resources; @Getter private final Map delayedRequests; @Getter + @Setter private final List pendingReadRequests; @Getter private final Queue queuedRequests; @Getter private boolean init; - @Getter - @Setter - private volatile boolean updateFw; public Object clone() throws CloneNotSupportedException { return super.clone(); @@ -110,9 +119,10 @@ public class LwM2mClient implements Cloneable { this.profileId = profileId; this.sessionId = sessionId; this.init = false; - this.updateFw = false; this.queuedRequests = new ConcurrentLinkedQueue<>(); - this.frUpdate = new LwM2mFirmwareUpdate(); + + this.fwUpdate = new LwM2mFwSwUpdate(this, FirmwareType.FIRMWARE); + this.swUpdate = new LwM2mFwSwUpdate(this, FirmwareType.SOFTWARE); if (this.credentials != null && this.credentials.hasDeviceInfo()) { this.session = createSession(nodeId, sessionId, credentials); this.deviceId = new UUID(session.getDeviceIdMSB(), session.getDeviceIdLSB()); @@ -165,15 +175,15 @@ public class LwM2mClient implements Cloneable { .build(); } - public boolean saveResourceValue(String pathRez, LwM2mResource rez, LwM2mModelProvider modelProvider) { - if (this.resources.get(pathRez) != null && this.resources.get(pathRez).getResourceModel() != null) { - this.resources.get(pathRez).setLwM2mResource(rez); + public boolean saveResourceValue(String pathRezIdVer, LwM2mResource rez, LwM2mModelProvider modelProvider) { + if (this.resources.get(pathRezIdVer) != null && this.resources.get(pathRezIdVer).getResourceModel() != null) { + this.resources.get(pathRezIdVer).setLwM2mResource(rez); return true; } else { - LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRez)); + LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRezIdVer)); ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); if (resourceModel != null) { - this.resources.put(pathRez, new ResourceValue(rez, resourceModel)); + this.resources.put(pathRezIdVer, new ResourceValue(rez, resourceModel)); return true; } else { return false; @@ -181,6 +191,24 @@ public class LwM2mClient implements Cloneable { } } + public Object getResourceValue(String pathRezIdVer, String pathRezId) { + String pathRez = pathRezIdVer == null ? convertPathFromObjectIdToIdVer(pathRezId, this.registration) : pathRezIdVer; + if (this.resources.get(pathRez) != null) { + return this.resources.get(pathRez).getLwM2mResource().isMultiInstances() ? + this.resources.get(pathRez).getLwM2mResource().getValues() : + this.resources.get(pathRez).getLwM2mResource().getValue(); + } + return null; + } + + public Object getResourceName (String pathRezIdVer, String pathRezId) { + String pathRez = pathRezIdVer == null ? convertPathFromObjectIdToIdVer(pathRezId, this.registration) : pathRezIdVer; + if (this.resources.get(pathRez) != null) { + return this.resources.get(pathRez).getResourceModel().name; + } + return null; + } + public ResourceModel getResourceModel(String pathIdVer, LwM2mModelProvider modelProvider) { LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer)); String verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId()); @@ -189,8 +217,55 @@ public class LwM2mClient implements Cloneable { .getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()) : null; } - public Collection getNewOneResourceForInstance(String pathRezIdVer, Object params, LwM2mModelProvider modelProvider, - LwM2mValueConverterImpl converter) { + public ObjectModel getObjectModel(String pathIdVer, LwM2mModelProvider modelProvider) { + LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer)); + String verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId()); + String verRez = getVerFromPathIdVerOrId(pathIdVer); + return verRez == null || verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration) + .getObjectModel(pathIds.getObjectId()) : null; + } + + public String objectToString (LwM2mObject lwM2mObject, LwM2mValueConverterImpl converter, String pathIdVer) { + StringBuilder builder = new StringBuilder(); + builder.append("LwM2mObject [id=").append(lwM2mObject.getId()).append(", instances={"); + lwM2mObject.getInstances().forEach((instId, inst) -> { + builder.append(instId).append("=").append(this.instanceToString(inst, converter, pathIdVer)).append(", "); + }); + int startInd = builder.lastIndexOf(", "); + if (startInd > 0) { + builder.delete(startInd, startInd + 2); + } + builder.append("}]"); + return builder.toString(); + } + public String instanceToString (LwM2mObjectInstance objectInstance, LwM2mValueConverterImpl converter, String pathIdVer) { + StringBuilder builder = new StringBuilder(); + builder.append("LwM2mObjectInstance [id=").append(objectInstance.getId()).append(", resources={"); + objectInstance.getResources().forEach((resId, res) -> { + builder.append(resId).append("=").append(this.resourceToString (res, converter, pathIdVer)).append(", "); + }); + int startInd = builder.lastIndexOf(", "); + if (startInd > 0) { + builder.delete(startInd, startInd + 2); + } + builder.append("}]"); + return builder.toString(); + } + + public String resourceToString (LwM2mResource lwM2mResource, LwM2mValueConverterImpl converter, String pathIdVer) { + if (!OPAQUE.equals(lwM2mResource.getType())) { + return lwM2mResource.isMultiInstances() ? ((LwM2mMultipleResource) lwM2mResource).toString() : + ((LwM2mSingleResource) lwM2mResource).toString(); + } + else { + return String.format("LwM2mSingleResource [id=%s, value=%s, type=%s]", lwM2mResource.getId(), + converter.convertValue(lwM2mResource.getValue(), + OPAQUE, STRING, new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer))), lwM2mResource.getType().name()); + } + } + + public Collection getNewResourceForInstance(String pathRezIdVer, Object params, LwM2mModelProvider modelProvider, + LwM2mValueConverterImpl converter) { LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRezIdVer)); Collection resources = ConcurrentHashMap.newKeySet(); Map resourceModels = modelProvider.getObjectModel(registration) @@ -204,8 +279,8 @@ public class LwM2mClient implements Cloneable { return resources; } - public Collection getNewManyResourcesForInstance(String pathRezIdVer, Object params, LwM2mModelProvider modelProvider, - LwM2mValueConverterImpl converter) { + public Collection getNewResourcesForInstance(String pathRezIdVer, Object params, LwM2mModelProvider modelProvider, + LwM2mValueConverterImpl converter) { LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRezIdVer)); Collection resources = ConcurrentHashMap.newKeySet(); Map resourceModels = modelProvider.getObjectModel(registration) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java index 4aa8db59b6..554a6e650c 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java @@ -55,7 +55,7 @@ public interface LwM2mClientContext { Map setProfiles(Map profiles); - LwM2mClientProfile toClientProfile(DeviceProfile deviceProfile); + LwM2mClientProfile profileUpdate(DeviceProfile deviceProfile); Set getSupportedIdVerInClient(Registration registration); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index cb662202f9..0a49ea82b2 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.server.client; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.security.EditableSecurityStore; @@ -40,6 +41,7 @@ import java.util.concurrent.ConcurrentHashMap; import static org.eclipse.leshan.core.SecurityMode.NO_SEC; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromObjectIdToIdVer; +@Slf4j @Service @TbLwM2mTransportComponent @RequiredArgsConstructor @@ -112,8 +114,12 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { EndpointSecurityInfo securityInfo = lwM2MCredentialsSecurityInfoValidator.getEndpointSecurityInfo(endpoint, LwM2mTransportUtil.LwM2mTypeServer.CLIENT); if (securityInfo.getSecurityMode() != null) { if (securityInfo.getDeviceProfile() != null) { - toClientProfile(securityInfo.getDeviceProfile()); - UUID profileUuid = securityInfo.getDeviceProfile().getUuidId(); + UUID profileUuid = profileUpdate(securityInfo.getDeviceProfile())!= null ? + securityInfo.getDeviceProfile().getUuidId() : null; + // TODO: for tests bug. + if (profileUuid== null) { + log.warn("input parameters toClientProfile if the result is null: [{}]", securityInfo.getDeviceProfile()); + } LwM2mClient client; if (securityInfo.getSecurityInfo() != null) { client = new LwM2mClient(context.getNodeId(), securityInfo.getSecurityInfo().getEndpoint(), @@ -141,7 +147,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { LwM2mClient client = new LwM2mClient(context.getNodeId(), registration.getEndpoint(), null, null, credentials, credentials.getDeviceProfile().getUuidId(), UUID.randomUUID()); lwM2mClientsByEndpoint.put(registration.getEndpoint(), client); lwM2mClientsByRegistrationId.put(registration.getId(), client); - toClientProfile(credentials.getDeviceProfile()); + profileUpdate(credentials.getDeviceProfile()); } @Override @@ -170,13 +176,16 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { } @Override - public LwM2mClientProfile toClientProfile(DeviceProfile deviceProfile) { - LwM2mClientProfile lwM2MClientProfile = profiles.get(deviceProfile.getUuidId()); - if (lwM2MClientProfile == null) { - lwM2MClientProfile = LwM2mTransportUtil.toLwM2MClientProfile(deviceProfile); + public LwM2mClientProfile profileUpdate(DeviceProfile deviceProfile) { + LwM2mClientProfile lwM2MClientProfile = deviceProfile != null ? + LwM2mTransportUtil.toLwM2MClientProfile(deviceProfile) : null; + if (lwM2MClientProfile != null) { profiles.put(deviceProfile.getUuidId(), lwM2MClientProfile); + return lwM2MClientProfile; + } + else { + return null; } - return lwM2MClientProfile; } /** diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mFirmwareUpdate.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mFirmwareUpdate.java deleted file mode 100644 index 9eb6321445..0000000000 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mFirmwareUpdate.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2021 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.transport.lwm2m.server.client; - -import lombok.Data; - -import java.util.UUID; - -@Data -public class LwM2mFirmwareUpdate { - private volatile String clientFwVersion; - private volatile String currentFwVersion; - private volatile UUID currentFwId; -} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mFwSwUpdate.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mFwSwUpdate.java new file mode 100644 index 0000000000..499d48a794 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mFwSwUpdate.java @@ -0,0 +1,355 @@ +/** + * Copyright © 2016-2021 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.transport.lwm2m.server.client; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.leshan.core.request.ContentFormat; +import org.thingsboard.server.common.data.firmware.FirmwareType; +import org.thingsboard.server.common.data.firmware.FirmwareUpdateStatus; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.lwm2m.server.DefaultLwM2MTransportMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportRequest; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; + +import static org.thingsboard.server.common.data.firmware.FirmwareKey.STATE; +import static org.thingsboard.server.common.data.firmware.FirmwareType.FIRMWARE; +import static org.thingsboard.server.common.data.firmware.FirmwareType.SOFTWARE; +import static org.thingsboard.server.common.data.firmware.FirmwareUpdateStatus.UPDATING; +import static org.thingsboard.server.common.data.firmware.FirmwareUtil.getAttributeKey; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_NAME_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_PACKAGE_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_RESULT_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_STATE_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_UPDATE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_UPDATE_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_VER_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_ERROR; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_INFO; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.EXECUTE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.OBSERVE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_REPLACE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_INSTALL_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_NAME_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_PACKAGE_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_RESULT_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_UN_INSTALL_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_UPDATE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_UPDATE_STATE_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_VER_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromObjectIdToIdVer; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.splitCamelCaseString; + +@Slf4j +public class LwM2mFwSwUpdate { + // 5/0/6 PkgName + // 9/0/0 PkgName + @Getter + @Setter + private volatile String currentTitle; + // 5/0/7 PkgVersion + // 9/0/1 PkgVersion + @Getter + @Setter + private volatile String currentVersion; + @Getter + @Setter + private volatile UUID currentId; + @Getter + @Setter + private volatile String stateUpdate; + @Getter + private String pathPackageId; + @Getter + private String pathStateId; + @Getter + private String pathResultId; + @Getter + private String pathNameId; + @Getter + private String pathVerId; + @Getter + private String pathInstallId; + @Getter + private String pathUnInstallId; + @Getter + private String wUpdate; + @Getter + @Setter + private volatile boolean infoFwSwUpdate = false; + private final FirmwareType type; + @Getter + LwM2mClient lwM2MClient; + @Getter + @Setter + private final List pendingInfoRequestsStart; + + public LwM2mFwSwUpdate(LwM2mClient lwM2MClient, FirmwareType type) { + this.lwM2MClient = lwM2MClient; + this.pendingInfoRequestsStart = new CopyOnWriteArrayList<>(); + this.type = type; + this.stateUpdate = null; + this.initPathId(); + } + + private void initPathId() { + if (FIRMWARE.equals(this.type)) { + this.pathPackageId = FW_PACKAGE_ID; + this.pathStateId = FW_STATE_ID; + this.pathResultId = FW_RESULT_ID; + this.pathNameId = FW_NAME_ID; + this.pathVerId = FW_VER_ID; + this.pathInstallId = FW_UPDATE_ID; + this.wUpdate = FW_UPDATE; + } else if (SOFTWARE.equals(this.type)) { + this.pathPackageId = SW_PACKAGE_ID; + this.pathStateId = SW_UPDATE_STATE_ID; + this.pathResultId = SW_RESULT_ID; + this.pathNameId = SW_NAME_ID; + this.pathVerId = SW_VER_ID; + this.pathInstallId = SW_INSTALL_ID; + this.pathUnInstallId = SW_UN_INSTALL_ID; + this.wUpdate = SW_UPDATE; + } + } + + public void initReadValue(DefaultLwM2MTransportMsgHandler handler, LwM2mTransportRequest request, String pathIdVer) { + if (pathIdVer != null) { + this.pendingInfoRequestsStart.remove(pathIdVer); + } + if (this.pendingInfoRequestsStart.size() == 0) { + this.infoFwSwUpdate = false; + if (!FirmwareUpdateStatus.DOWNLOADING.name().equals(this.stateUpdate)) { + boolean conditionalStart = this.type.equals(FIRMWARE) ? this.conditionalFwUpdateStart() : + this.conditionalSwUpdateStart(); + if (conditionalStart) { + this.writeFwSwWare(handler, request); + } + } + } + } + + /** + * Send FsSw to Lwm2mClient: + * before operation Write: fw_state = DOWNLOADING + */ + private void writeFwSwWare(DefaultLwM2MTransportMsgHandler handler, LwM2mTransportRequest request) { + this.stateUpdate = FirmwareUpdateStatus.DOWNLOADING.name(); +// this.observeStateUpdate(); + this.sendLogs(handler, WRITE_REPLACE.name(), LOG_LW2M_INFO, null); + int chunkSize = 0; + int chunk = 0; + byte[] firmwareChunk = handler.firmwareDataCache.get(this.currentId.toString(), chunkSize, chunk); + String targetIdVer = convertPathFromObjectIdToIdVer(this.pathPackageId, this.lwM2MClient.getRegistration()); + request.sendAllRequest(lwM2MClient.getRegistration(), targetIdVer, WRITE_REPLACE, ContentFormat.OPAQUE.getName(), + firmwareChunk, handler.config.getTimeout(), null); + } + + public void sendLogs(DefaultLwM2MTransportMsgHandler handler, String typeOper, String typeInfo, String msgError) { + this.sendSateOnThingsBoard(handler); + String msg = String.format("%s: %s, %s, pkgVer: %s: pkgName - %s state - %s.", + typeInfo, this.wUpdate, typeOper, this.currentVersion, this.currentTitle, this.stateUpdate); + if (LOG_LW2M_ERROR.equals(typeInfo)) { + msg = String.format("%s Error: %s", msg, msgError); + } + handler.sendLogsToThingsboard(msg, lwM2MClient.getRegistration().getId()); + } + + + /** + * After inspection Update Result + * fw_state/sw_state = UPDATING + * send execute + */ + public void executeFwSwWare(DefaultLwM2MTransportMsgHandler handler, LwM2mTransportRequest request) { + this.setStateUpdate(UPDATING.name()); + this.sendLogs(handler, EXECUTE.name(), LOG_LW2M_INFO, null); + request.sendAllRequest(this.lwM2MClient.getRegistration(), this.pathInstallId, EXECUTE, ContentFormat.TLV.getName(), + null, 0, null); + } + + + /** + * Firmware start: + * -- Если Update Result -errors (более 1) - Это означает что пред. апдейт не прошел. + * - Запускаем апдейт в независимости от состяния прошивки и ее версии. + * -- Если Update Result - не errors (менее или равно 1) и ver не пустой - Это означает что пред. апдейт прошел. + * -- Если Update Result - не errors и ver пустой - Это означает что апдейта еще не было. + * - Проверяем поменялась ли версия и запускаем новый апдейт. + */ + private boolean conditionalFwUpdateStart() { + Long updateResultFw = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); + // #1/#2 + return updateResultFw > LwM2mTransportUtil.UpdateResultFw.UPDATE_SUCCESSFULLY.code || + ( + (updateResultFw <= LwM2mTransportUtil.UpdateResultFw.UPDATE_SUCCESSFULLY.code + ) && + ( + (this.currentVersion != null && !this.currentVersion.equals(this.lwM2MClient.getResourceValue(null, this.pathVerId))) || + (this.currentTitle != null && !this.currentTitle.equals(this.lwM2MClient.getResourceValue(null, this.pathNameId))) + ) + ); + } + + + /** + * Before operation Execute inspection Update Result : + * 0 - Initial value + */ + public boolean conditionalFwExecuteStart() { + Long updateResult = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); + return LwM2mTransportUtil.UpdateResultFw.INITIAL.code == updateResult; + } + + /** + * After operation Execute success inspection Update Result : + * 1 - "Firmware updated successfully" + */ + public boolean conditionalFwExecuteAfterSuccess() { + Long updateResult = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); + return LwM2mTransportUtil.UpdateResultFw.UPDATE_SUCCESSFULLY.code == updateResult; + } + + /** + * After operation Execute success inspection Update Result : + * > 1 error: "Firmware updated successfully" + */ + public boolean conditionalFwExecuteAfterError() { + Long updateResult = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); + return LwM2mTransportUtil.UpdateResultFw.UPDATE_SUCCESSFULLY.code < updateResult; + } + + /** + * Software start + * -- Если Update Result -errors (равно и более 50) - Это означает что пред. апдейт не прошел. + * * - Запускаем апдейт в независимости от состяния прошивки и ее версии. + * -- Если Update Result - не errors (менее 50) и ver не пустой - Это означает что пред. апдейт прошел. + * -- Если Update Result - не errors и ver пустой - Это означает что апдейта еще не было или пред. апдейт UnInstall + * -- Если Update Result - не errors и ver не пустой - Это означает что пред. апдейт UnInstall + * - Проверяем поменялась ли версия и запускаем новый апдейт. + */ + private boolean conditionalSwUpdateStart() { + Long updateResultSw = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); + // #1/#2 + return updateResultSw >= LwM2mTransportUtil.UpdateResultSw.NOT_ENOUGH_STORAGE.code || + ( + (updateResultSw <= LwM2mTransportUtil.UpdateResultSw.NOT_ENOUGH_STORAGE.code + ) && + ( + (this.currentVersion != null && !this.currentVersion.equals(this.lwM2MClient.getResourceValue(null, this.pathVerId))) || + (this.currentTitle != null && !this.currentTitle.equals(this.lwM2MClient.getResourceValue(null, this.pathNameId))) + ) + ); + } + + /** + * Before operation Execute inspection Update Result : + * 3 - Successfully Downloaded and package integrity verified + */ + public boolean conditionalSwUpdateExecute() { + Long updateResult = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); + return LwM2mTransportUtil.UpdateResultSw.SUCCESSFULLY_DOWNLOADED_VERIFIED.code == updateResult; + } + + /** + * After finish operation Execute (success): + * -- inspection Update Result: + * ---- FW если Update Result == 1 ("Firmware updated successfully") или SW если Update Result == 2 ("Software successfully installed.") + * -- fw_state/sw_state = UPDATED + * + * After finish operation Execute (error): + * -- inspection updateResult and send to thingsboard info about error + * --- send to telemetry ( key - this is name Update Result in model) ( + * -- fw_state/sw_state = FAILED + */ + public void finishFwSwUpdate(DefaultLwM2MTransportMsgHandler handler, boolean success) { + Long updateResult = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); + String value = FIRMWARE.equals(this.type) ? LwM2mTransportUtil.UpdateResultFw.fromUpdateResultFwByCode(updateResult.intValue()).type : + LwM2mTransportUtil.UpdateResultSw.fromUpdateResultSwByCode(updateResult.intValue()).type; + String key = splitCamelCaseString((String) this.lwM2MClient.getResourceName(null, this.pathResultId)); + if (success) { + this.stateUpdate = FirmwareUpdateStatus.UPDATED.name(); + this.sendLogs(handler, EXECUTE.name(), LOG_LW2M_INFO, null); + } else { + this.stateUpdate = FirmwareUpdateStatus.FAILED.name(); + this.sendLogs(handler, EXECUTE.name(), LOG_LW2M_ERROR, value); + } + handler.helper.sendParametersOnThingsboardTelemetry( + handler.helper.getKvStringtoThingsboard(key, value), this.lwM2MClient.getSession()); + } + + /** + * After operation Execute success inspection Update Result : + * 2 - "Software successfully installed." + */ + public boolean conditionalSwExecuteAfterSuccess() { + Long updateResult = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); + return LwM2mTransportUtil.UpdateResultSw.SUCCESSFULLY_INSTALLED.code == updateResult; + } + + /** + * After operation Execute success inspection Update Result : + * >= 50 - error "NOT_ENOUGH_STORAGE" + */ + public boolean conditionalSwExecuteAfterError() { + Long updateResult = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); + return LwM2mTransportUtil.UpdateResultSw.NOT_ENOUGH_STORAGE.code <= updateResult; + } + + private void observeStateUpdate(DefaultLwM2MTransportMsgHandler handler, LwM2mTransportRequest request) { + request.sendAllRequest(lwM2MClient.getRegistration(), + convertPathFromObjectIdToIdVer(this.pathStateId, this.lwM2MClient.getRegistration()), OBSERVE, + null, null, 0, null); + request.sendAllRequest(lwM2MClient.getRegistration(), + convertPathFromObjectIdToIdVer(this.pathResultId, this.lwM2MClient.getRegistration()), OBSERVE, + null, null, 0, null); + } + + public void sendSateOnThingsBoard(DefaultLwM2MTransportMsgHandler handler) { + if (StringUtils.trimToNull(this.stateUpdate) != null) { + List result = new ArrayList<>(); + TransportProtos.KeyValueProto.Builder kvProto = TransportProtos.KeyValueProto.newBuilder().setKey(getAttributeKey(this.type, STATE)); + kvProto.setType(TransportProtos.KeyValueType.STRING_V).setStringV(stateUpdate); + result.add(kvProto.build()); + handler.helper.sendParametersOnThingsboardTelemetry(result, + handler.getSessionInfoOrCloseSession(this.lwM2MClient.getRegistration())); + } + } + + public void sendReadObserveInfo(LwM2mTransportRequest request) { + this.infoFwSwUpdate = true; + this.pendingInfoRequestsStart.add(convertPathFromObjectIdToIdVer( + this.pathVerId, this.lwM2MClient.getRegistration())); + this.pendingInfoRequestsStart.add(convertPathFromObjectIdToIdVer( + this.pathNameId, this.lwM2MClient.getRegistration())); + this.pendingInfoRequestsStart.add(convertPathFromObjectIdToIdVer( + this.pathStateId, this.lwM2MClient.getRegistration())); + this.pendingInfoRequestsStart.add(convertPathFromObjectIdToIdVer( + this.pathResultId, this.lwM2MClient.getRegistration())); + this.pendingInfoRequestsStart.forEach(pathIdVer -> { + request.sendAllRequest(this.lwM2MClient.getRegistration(), pathIdVer, OBSERVE, ContentFormat.TLV.getName(), + null, 0, null); + }); + + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/Lwm2mClientRpcRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/Lwm2mClientRpcRequest.java index 4734b5dd8e..77a26b4755 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/Lwm2mClientRpcRequest.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/Lwm2mClientRpcRequest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.lwm2m.server.client; import com.google.gson.JsonObject; import lombok.Data; +import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.server.registration.Registration; import org.thingsboard.server.gen.transport.TransportProtos; @@ -27,6 +28,7 @@ import java.util.concurrent.ConcurrentHashMap; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.validPathIdVer; +@Slf4j @Data public class Lwm2mClientRpcRequest { public final String targetIdVerKey = "targetIdVer"; @@ -107,4 +109,14 @@ public class Lwm2mClientRpcRequest { .setRequestId(this.requestId) .build(); } + + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + log.error("", e); + } + return null; + } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index d947e22133..1de2c1a4aa 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -26,9 +26,7 @@ import org.eclipse.leshan.core.util.NamedThreadFactory; import org.eclipse.leshan.core.util.Validate; import org.eclipse.leshan.server.californium.observation.ObserveUtil; import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; -import org.eclipse.leshan.server.redis.JedisLock; import org.eclipse.leshan.server.redis.RedisRegistrationStore; -import org.eclipse.leshan.server.redis.SingleInstanceJedisLock; import org.eclipse.leshan.server.redis.serialization.ObservationSerDes; import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes; import org.eclipse.leshan.server.registration.Deregistration; @@ -38,11 +36,12 @@ import org.eclipse.leshan.server.registration.RegistrationUpdate; import org.eclipse.leshan.server.registration.UpdatedRegistration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.redis.connection.RedisClusterConnection; +import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.ScanParams; -import redis.clients.jedis.ScanResult; -import redis.clients.jedis.Transaction; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.integration.redis.util.RedisLockRegistry; import java.net.InetSocketAddress; import java.util.ArrayList; @@ -50,13 +49,14 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; -import java.util.NoSuchElementException; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; import static java.nio.charset.StandardCharsets.UTF_8; @@ -92,7 +92,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto private final int cleanLimit; // maximum number to clean in a clean period private final long gracePeriod; // in seconds - private final JedisLock lock; + private final RedisLockRegistry redisLock; public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory) { this(connectionFactory, DEFAULT_CLEAN_PERIOD, DEFAULT_GRACE_PERIOD, DEFAULT_CLEAN_LIMIT); // default clean period 60s @@ -106,20 +106,12 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, long lifetimeGracePeriodInSec, int cleanLimit) { - this(connectionFactory, schedExecutor, cleanPeriodInSec, lifetimeGracePeriodInSec, cleanLimit, new SingleInstanceJedisLock()); - } - - /** - * @since 1.1 - */ - public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, - long lifetimeGracePeriodInSec, int cleanLimit, JedisLock redisLock) { this.connectionFactory = connectionFactory; this.schedExecutor = schedExecutor; this.cleanPeriod = cleanPeriodInSec; this.cleanLimit = cleanLimit; this.gracePeriod = lifetimeGracePeriodInSec; - this.lock = redisLock; + this.redisLock = new RedisLockRegistry(connectionFactory, "Registration"); } /* *************** Redis Key utility function **************** */ @@ -135,76 +127,79 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto return (prefix + registrationID).getBytes(); } - private byte[] toLockKey(String endpoint) { - return toKey(LOCK_EP, endpoint); + private String toLockKey(String endpoint) { + return new String(toKey(LOCK_EP, endpoint)); } - private byte[] toLockKey(byte[] endpoint) { - return toKey(LOCK_EP.getBytes(UTF_8), endpoint); + private String toLockKey(byte[] endpoint) { + return new String(toKey(LOCK_EP.getBytes(UTF_8), endpoint)); } /* *************** Leshan Registration API **************** */ @Override public Deregistration addRegistration(Registration registration) { - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { - byte[] lockValue = null; - byte[] lockKey = toLockKey(registration.getEndpoint()); + Lock lock = null; + try (var connection = connectionFactory.getConnection()) { + String lockKey = toLockKey(registration.getEndpoint()); try { - lockValue = lock.acquire(j, lockKey); - + lock = redisLock.obtain(lockKey); + lock.lock(); // add registration byte[] k = toEndpointKey(registration.getEndpoint()); - byte[] old = j.getSet(k, serializeReg(registration)); + byte[] old = connection.getSet(k, serializeReg(registration)); // add registration: secondary indexes byte[] regid_idx = toRegIdKey(registration.getId()); - j.set(regid_idx, registration.getEndpoint().getBytes(UTF_8)); + connection.set(regid_idx, registration.getEndpoint().getBytes(UTF_8)); byte[] addr_idx = toRegAddrKey(registration.getSocketAddress()); - j.set(addr_idx, registration.getEndpoint().getBytes(UTF_8)); + connection.set(addr_idx, registration.getEndpoint().getBytes(UTF_8)); // Add or update expiration - addOrUpdateExpiration(j, registration); + addOrUpdateExpiration(connection, registration); if (old != null) { Registration oldRegistration = deserializeReg(old); // remove old secondary index if (!registration.getId().equals(oldRegistration.getId())) - j.del(toRegIdKey(oldRegistration.getId())); + connection.del(toRegIdKey(oldRegistration.getId())); if (!oldRegistration.getSocketAddress().equals(registration.getSocketAddress())) { - removeAddrIndex(j, oldRegistration); + removeAddrIndex(connection, oldRegistration); } // remove old observation - Collection obsRemoved = unsafeRemoveAllObservations(j, oldRegistration.getId()); + Collection obsRemoved = unsafeRemoveAllObservations(connection, oldRegistration.getId()); return new Deregistration(oldRegistration, obsRemoved); } return null; } finally { - lock.release(j, lockKey, lockValue); + if (lock != null) { + lock.unlock(); + } } } } @Override public UpdatedRegistration updateRegistration(RegistrationUpdate update) { - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + Lock lock = null; + try (var connection = connectionFactory.getConnection()) { // Fetch the registration ep by registration ID index - byte[] ep = j.get(toRegIdKey(update.getRegistrationId())); + byte[] ep = connection.get(toRegIdKey(update.getRegistrationId())); if (ep == null) { return null; } - byte[] lockValue = null; - byte[] lockKey = toLockKey(ep); + String lockKey = toLockKey(ep); try { - lockValue = lock.acquire(j, lockKey); + lock = redisLock.obtain(lockKey); + lock.lock(); // Fetch the registration - byte[] data = j.get(toEndpointKey(ep)); + byte[] data = connection.get(toEndpointKey(ep)); if (data == null) { return null; } @@ -214,40 +209,42 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto Registration updatedRegistration = update.update(r); // Store the new registration - j.set(toEndpointKey(updatedRegistration.getEndpoint()), serializeReg(updatedRegistration)); + connection.set(toEndpointKey(updatedRegistration.getEndpoint()), serializeReg(updatedRegistration)); // Add or update expiration - addOrUpdateExpiration(j, updatedRegistration); + addOrUpdateExpiration(connection, updatedRegistration); // Update secondary index : // If registration is already associated to this address we don't care as we only want to keep the most // recent binding. byte[] addr_idx = toRegAddrKey(updatedRegistration.getSocketAddress()); - j.set(addr_idx, updatedRegistration.getEndpoint().getBytes(UTF_8)); + connection.set(addr_idx, updatedRegistration.getEndpoint().getBytes(UTF_8)); if (!r.getSocketAddress().equals(updatedRegistration.getSocketAddress())) { - removeAddrIndex(j, r); + removeAddrIndex(connection, r); } return new UpdatedRegistration(r, updatedRegistration); } finally { - lock.release(j, lockKey, lockValue); + if (lock != null) { + lock.unlock(); + } } } } @Override public Registration getRegistration(String registrationId) { - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { - return getRegistration(j, registrationId); + try (var connection = connectionFactory.getConnection()) { + return getRegistration(connection, registrationId); } } @Override public Registration getRegistrationByEndpoint(String endpoint) { Validate.notNull(endpoint); - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { - byte[] data = j.get(toEndpointKey(endpoint)); + try (var connection = connectionFactory.getConnection()) { + byte[] data = connection.get(toEndpointKey(endpoint)); if (data == null) { return null; } @@ -258,12 +255,12 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto @Override public Registration getRegistrationByAdress(InetSocketAddress address) { Validate.notNull(address); - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { - byte[] ep = j.get(toRegAddrKey(address)); + try (var connection = connectionFactory.getConnection()) { + byte[] ep = connection.get(toRegAddrKey(address)); if (ep == null) { return null; } - byte[] data = j.get(toEndpointKey(ep)); + byte[] data = connection.get(toEndpointKey(ep)); if (data == null) { return null; } @@ -273,140 +270,99 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto @Override public Iterator getAllRegistrations() { - return new TbLwM2mRedisRegistrationStore.RedisIterator(connectionFactory, new ScanParams().match(REG_EP + "*").count(100)); - } - - protected class RedisIterator implements Iterator { - - private final RedisConnectionFactory connectionFactory; - private final ScanParams scanParams; - - private String cursor; - private List scanResult; - - public RedisIterator(RedisConnectionFactory connectionFactory, ScanParams scanParams) { - this.connectionFactory = connectionFactory; - this.scanParams = scanParams; - // init scan result - scanNext("0"); - } - - private void scanNext(String cursor) { - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { - do { - ScanResult sr = j.scan(cursor.getBytes(), scanParams); - - this.scanResult = new ArrayList<>(); - if (sr.getResult() != null && !sr.getResult().isEmpty()) { - for (byte[] value : j.mget(sr.getResult().toArray(new byte[][]{}))) { - this.scanResult.add(deserializeReg(value)); - } - } - - cursor = sr.getCursor(); - } while (!"0".equals(cursor) && scanResult.isEmpty()); - - this.cursor = cursor; - } - } - - @Override - public boolean hasNext() { - if (!scanResult.isEmpty()) { - return true; - } - if ("0".equals(cursor)) { - // no more elements to scan - return false; - } - - // read more elements - scanNext(cursor); - return !scanResult.isEmpty(); - } - - @Override - public Registration next() { - if (!hasNext()) { - throw new NoSuchElementException(); + try (var connection = connectionFactory.getConnection()) { + Collection list = new LinkedList<>(); + ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(REG_EP + "*").build(); + List> scans = new ArrayList<>(); + if (connection instanceof RedisClusterConnection) { + ((RedisClusterConnection) connection).clusterGetNodes().forEach(node -> { + scans.add(((RedisClusterConnection) connection).scan(node, scanOptions)); + }); + } else { + scans.add(connection.scan(scanOptions)); } - return scanResult.remove(0); - } - @Override - public void remove() { - throw new UnsupportedOperationException(); + scans.forEach(scan -> { + scan.forEachRemaining(key -> { + byte[] element = connection.get(key); + list.add(deserializeReg(element)); + }); + }); + return list.iterator(); } } @Override public Deregistration removeRegistration(String registrationId) { - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { - return removeRegistration(j, registrationId, false); + try (var connection = connectionFactory.getConnection()) { + return removeRegistration(connection, registrationId, false); } } - private Deregistration removeRegistration(Jedis j, String registrationId, boolean removeOnlyIfNotAlive) { + private Deregistration removeRegistration(RedisConnection connection, String registrationId, boolean removeOnlyIfNotAlive) { // fetch the client ep by registration ID index - byte[] ep = j.get(toRegIdKey(registrationId)); + byte[] ep = connection.get(toRegIdKey(registrationId)); if (ep == null) { return null; } - byte[] lockValue = null; - byte[] lockKey = toLockKey(ep); + Lock lock = null; + String lockKey = toLockKey(ep); try { - lockValue = lock.acquire(j, lockKey); + lock = redisLock.obtain(lockKey); + lock.lock(); // fetch the client - byte[] data = j.get(toEndpointKey(ep)); + byte[] data = connection.get(toEndpointKey(ep)); if (data == null) { return null; } Registration r = deserializeReg(data); if (!removeOnlyIfNotAlive || !r.isAlive(gracePeriod)) { - long nbRemoved = j.del(toRegIdKey(r.getId())); + long nbRemoved = connection.del(toRegIdKey(r.getId())); if (nbRemoved > 0) { - j.del(toEndpointKey(r.getEndpoint())); - Collection obsRemoved = unsafeRemoveAllObservations(j, r.getId()); - removeAddrIndex(j, r); - removeExpiration(j, r); + connection.del(toEndpointKey(r.getEndpoint())); + Collection obsRemoved = unsafeRemoveAllObservations(connection, r.getId()); + removeAddrIndex(connection, r); + removeExpiration(connection, r); return new Deregistration(r, obsRemoved); } } return null; } finally { - lock.release(j, lockKey, lockValue); + if (lock != null) { + lock.unlock(); + } } } - private void removeAddrIndex(Jedis j, Registration registration) { + private void removeAddrIndex(RedisConnection connection, Registration registration) { // Watch the key to remove. byte[] regAddrKey = toRegAddrKey(registration.getSocketAddress()); - j.watch(regAddrKey); + connection.watch(regAddrKey); - byte[] epFromAddr = j.get(regAddrKey); + byte[] epFromAddr = connection.get(regAddrKey); // Delete the key if needed. if (Arrays.equals(epFromAddr, registration.getEndpoint().getBytes(UTF_8))) { // Try to delete the key - Transaction transaction = j.multi(); - transaction.del(regAddrKey); - transaction.exec(); + connection.multi(); + connection.del(regAddrKey); + connection.exec(); // if transaction failed this is not an issue as the socket address is probably reused and we don't neeed to // delete it anymore. } else { // the key must not be deleted. - j.unwatch(); + connection.unwatch(); } } - private void addOrUpdateExpiration(Jedis j, Registration registration) { - j.zadd(EXP_EP, registration.getExpirationTimeStamp(gracePeriod), registration.getEndpoint().getBytes(UTF_8)); + private void addOrUpdateExpiration(RedisConnection connection, Registration registration) { + connection.zAdd(EXP_EP, registration.getExpirationTimeStamp(gracePeriod), registration.getEndpoint().getBytes(UTF_8)); } - private void removeExpiration(Jedis j, Registration registration) { - j.zrem(EXP_EP, registration.getEndpoint().getBytes(UTF_8)); + private void removeExpiration(RedisConnection connection, Registration registration) { + connection.zRem(EXP_EP, registration.getEndpoint().getBytes(UTF_8)); } private byte[] toRegIdKey(String registrationId) { @@ -441,33 +397,35 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto */ @Override public Collection addObservation(String registrationId, Observation observation) { - List removed = new ArrayList<>(); - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + try (var connection = connectionFactory.getConnection()) { // fetch the client ep by registration ID index - byte[] ep = j.get(toRegIdKey(registrationId)); + byte[] ep = connection.get(toRegIdKey(registrationId)); if (ep == null) { return null; } - byte[] lockValue = null; - byte[] lockKey = toLockKey(ep); + Lock lock = null; + String lockKey = toLockKey(ep); try { - lockValue = lock.acquire(j, lockKey); + lock = redisLock.obtain(lockKey); + lock.lock(); // cancel existing observations for the same path and registration id. - for (Observation obs : getObservations(j, registrationId)) { + for (Observation obs : getObservations(connection, registrationId)) { if (observation.getPath().equals(obs.getPath()) && !Arrays.equals(observation.getId(), obs.getId())) { removed.add(obs); - unsafeRemoveObservation(j, registrationId, obs.getId()); + unsafeRemoveObservation(connection, registrationId, obs.getId()); } } } finally { - lock.release(j, lockKey, lockValue); + if (lock != null) { + lock.unlock(); + } } } return removed; @@ -475,29 +433,32 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto @Override public Observation removeObservation(String registrationId, byte[] observationId) { - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + try (var connection = connectionFactory.getConnection()) { // fetch the client ep by registration ID index - byte[] ep = j.get(toRegIdKey(registrationId)); + byte[] ep = connection.get(toRegIdKey(registrationId)); if (ep == null) { return null; } // remove observation - byte[] lockValue = null; - byte[] lockKey = toLockKey(ep); + Lock lock = null; + String lockKey = toLockKey(ep); try { - lockValue = lock.acquire(j, lockKey); + lock = redisLock.obtain(lockKey); + lock.lock(); Observation observation = build(get(new Token(observationId))); if (observation != null && registrationId.equals(observation.getRegistrationId())) { - unsafeRemoveObservation(j, registrationId, observationId); + unsafeRemoveObservation(connection, registrationId, observationId); return observation; } return null; } finally { - lock.release(j, lockKey, lockValue); + if (lock != null) { + lock.unlock(); + } } } } @@ -509,15 +470,15 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto @Override public Collection getObservations(String registrationId) { - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { - return getObservations(j, registrationId); + try (var connection = connectionFactory.getConnection()) { + return getObservations(connection, registrationId); } } - private Collection getObservations(Jedis j, String registrationId) { + private Collection getObservations(RedisConnection connection, String registrationId) { Collection result = new ArrayList<>(); - for (byte[] token : j.lrange(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, -1)) { - byte[] obs = j.get(toKey(OBS_TKN, token)); + for (byte[] token : connection.lRange(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, -1)) { + byte[] obs = connection.get(toKey(OBS_TKN, token)); if (obs != null) { result.add(build(deserializeObs(obs))); } @@ -527,22 +488,24 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto @Override public Collection removeObservations(String registrationId) { - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + try (var connection = connectionFactory.getConnection()) { // check registration exists - Registration registration = getRegistration(j, registrationId); + Registration registration = getRegistration(connection, registrationId); if (registration == null) return Collections.emptyList(); // get endpoint and create lock String endpoint = registration.getEndpoint(); - byte[] lockValue = null; - byte[] lockKey = toKey(LOCK_EP, endpoint); + Lock lock = null; + String lockKey = toLockKey(endpoint); try { - lockValue = lock.acquire(j, lockKey); - - return unsafeRemoveAllObservations(j, registrationId); + lock = redisLock.obtain(lockKey); + lock.lock(); + return unsafeRemoveAllObservations(connection, registrationId); } finally { - lock.release(j, lockKey, lockValue); + if (lock != null) { + lock.unlock(); + } } } } @@ -565,31 +528,32 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto String endpoint = ObserveUtil.validateCoapObservation(obs); org.eclipse.californium.core.observe.Observation previousObservation = null; - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { - byte[] lockValue = null; - byte[] lockKey = toKey(LOCK_EP, endpoint); + try (var connection = connectionFactory.getConnection()) { + Lock lock = null; + String lockKey = toLockKey(endpoint); try { - lockValue = lock.acquire(j, lockKey); + lock = redisLock.obtain(lockKey); + lock.lock(); String registrationId = ObserveUtil.extractRegistrationId(obs); - if (!j.exists(toRegIdKey(registrationId))) + if (!connection.exists(toRegIdKey(registrationId))) throw new ObservationStoreException("no registration for this Id"); byte[] key = toKey(OBS_TKN, obs.getRequest().getToken().getBytes()); byte[] serializeObs = serializeObs(obs); byte[] previousValue; if (ifAbsent) { - previousValue = j.get(key); + previousValue = connection.get(key); if (previousValue == null || previousValue.length == 0) { - j.set(key, serializeObs); + connection.set(key, serializeObs); } else { return deserializeObs(previousValue); } } else { - previousValue = j.getSet(key, serializeObs); + previousValue = connection.getSet(key, serializeObs); } // secondary index to get the list by registrationId - j.lpush(toKey(OBS_TKNS_REGID_IDX, registrationId), obs.getRequest().getToken().getBytes()); + connection.lPush(toKey(OBS_TKNS_REGID_IDX, registrationId), obs.getRequest().getToken().getBytes()); // log any collisions if (previousValue != null && previousValue.length != 0) { @@ -599,7 +563,9 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto previousObservation.getRequest(), obs.getRequest()); } } finally { - lock.release(j, lockKey, lockValue); + if (lock != null) { + lock.unlock(); + } } } return previousObservation; @@ -607,17 +573,17 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto @Override public void remove(Token token) { - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + try (var connection = connectionFactory.getConnection()) { byte[] tokenKey = toKey(OBS_TKN, token.getBytes()); // fetch the observation by token - byte[] serializedObs = j.get(tokenKey); + byte[] serializedObs = connection.get(tokenKey); if (serializedObs == null) return; org.eclipse.californium.core.observe.Observation obs = deserializeObs(serializedObs); String registrationId = ObserveUtil.extractRegistrationId(obs); - Registration registration = getRegistration(j, registrationId); + Registration registration = getRegistration(connection, registrationId); if (registration == null) { LOG.warn("Unable to remove observation {}, registration {} does not exist anymore", obs.getRequest(), registrationId); @@ -625,14 +591,17 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto } String endpoint = registration.getEndpoint(); - byte[] lockValue = null; - byte[] lockKey = toKey(LOCK_EP, endpoint); + Lock lock = null; + String lockKey = toLockKey(endpoint); try { - lockValue = lock.acquire(j, lockKey); + lock = redisLock.obtain(lockKey); + lock.lock(); - unsafeRemoveObservation(j, registrationId, token.getBytes()); + unsafeRemoveObservation(connection, registrationId, token.getBytes()); } finally { - lock.release(j, lockKey, lockValue); + if (lock != null) { + lock.unlock(); + } } } @@ -640,8 +609,8 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto @Override public org.eclipse.californium.core.observe.Observation get(Token token) { - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { - byte[] obs = j.get(toKey(OBS_TKN, token.getBytes())); + try (var connection = connectionFactory.getConnection()) { + byte[] obs = connection.get(toKey(OBS_TKN, token.getBytes())); if (obs == null) { return null; } else { @@ -652,12 +621,12 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto /* *************** Observation utility functions **************** */ - private Registration getRegistration(Jedis j, String registrationId) { - byte[] ep = j.get(toRegIdKey(registrationId)); + private Registration getRegistration(RedisConnection connection, String registrationId) { + byte[] ep = connection.get(toRegIdKey(registrationId)); if (ep == null) { return null; } - byte[] data = j.get(toEndpointKey(ep)); + byte[] data = connection.get(toEndpointKey(ep)); if (data == null) { return null; } @@ -665,25 +634,25 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto return deserializeReg(data); } - private void unsafeRemoveObservation(Jedis j, String registrationId, byte[] observationId) { - if (j.del(toKey(OBS_TKN, observationId)) > 0L) { - j.lrem(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, observationId); + private void unsafeRemoveObservation(RedisConnection connection, String registrationId, byte[] observationId) { + if (connection.del(toKey(OBS_TKN, observationId)) > 0L) { + connection.lRem(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, observationId); } } - private Collection unsafeRemoveAllObservations(Jedis j, String registrationId) { + private Collection unsafeRemoveAllObservations(RedisConnection connection, String registrationId) { Collection removed = new ArrayList<>(); byte[] regIdKey = toKey(OBS_TKNS_REGID_IDX, registrationId); // fetch all observations by token - for (byte[] token : j.lrange(regIdKey, 0, -1)) { - byte[] obs = j.get(toKey(OBS_TKN, token)); + for (byte[] token : connection.lRange(regIdKey, 0, -1)) { + byte[] obs = connection.get(toKey(OBS_TKN, token)); if (obs != null) { removed.add(build(deserializeObs(obs))); } - j.del(toKey(OBS_TKN, token)); + connection.del(toKey(OBS_TKN, token)); } - j.del(regIdKey); + connection.del(regIdKey); return removed; } @@ -754,14 +723,14 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto @Override public void run() { - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { - Set endpointsExpired = j.zrangeByScore(EXP_EP, Double.NEGATIVE_INFINITY, + try (var connection = connectionFactory.getConnection()) { + Set endpointsExpired = connection.zRangeByScore(EXP_EP, Double.NEGATIVE_INFINITY, System.currentTimeMillis(), 0, cleanLimit); for (byte[] endpoint : endpointsExpired) { - Registration r = deserializeReg(j.get(toEndpointKey(endpoint))); + Registration r = deserializeReg(connection.get(toEndpointKey(endpoint))); if (!r.isAlive(gracePeriod)) { - Deregistration dereg = removeRegistration(j, r.getId(), true); + Deregistration dereg = removeRegistration(connection, r.getId(), true); if (dereg != null) expirationListener.registrationExpired(dereg.getRegistration(), dereg.getObservations()); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java index 47b13bb72b..4cfe2a6829 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java @@ -20,13 +20,15 @@ import org.eclipse.leshan.server.security.EditableSecurityStore; import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException; import org.eclipse.leshan.server.security.SecurityInfo; import org.eclipse.leshan.server.security.SecurityStoreListener; +import org.springframework.data.redis.connection.RedisClusterConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.ScanParams; -import redis.clients.jedis.ScanResult; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; +import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; +import java.util.List; public class TbLwM2mRedisSecurityStore implements EditableSecurityStore { private static final String SEC_EP = "SEC#EP#"; @@ -42,8 +44,8 @@ public class TbLwM2mRedisSecurityStore implements EditableSecurityStore { @Override public SecurityInfo getByEndpoint(String endpoint) { - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { - byte[] data = j.get((SEC_EP + endpoint).getBytes()); + try (var connection = connectionFactory.getConnection()) { + byte[] data = connection.get((SEC_EP + endpoint).getBytes()); if (data == null) { return null; } else { @@ -54,12 +56,12 @@ public class TbLwM2mRedisSecurityStore implements EditableSecurityStore { @Override public SecurityInfo getByIdentity(String identity) { - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { - String ep = j.hget(PSKID_SEC, identity); + try (var connection = connectionFactory.getConnection()) { + byte[] ep = connection.hGet(PSKID_SEC.getBytes(), identity.getBytes()); if (ep == null) { return null; } else { - byte[] data = j.get((SEC_EP + ep).getBytes()); + byte[] data = connection.get((SEC_EP + new String(ep)).getBytes()); if (data == null) { return null; } else { @@ -71,18 +73,24 @@ public class TbLwM2mRedisSecurityStore implements EditableSecurityStore { @Override public Collection getAll() { - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { - ScanParams params = new ScanParams().match(SEC_EP + "*").count(100); + try (var connection = connectionFactory.getConnection()) { Collection list = new LinkedList<>(); - String cursor = "0"; - do { - ScanResult res = j.scan(cursor.getBytes(), params); - for (byte[] key : res.getResult()) { - byte[] element = j.get(key); + ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(SEC_EP + "*").build(); + List> scans = new ArrayList<>(); + if (connection instanceof RedisClusterConnection) { + ((RedisClusterConnection) connection).clusterGetNodes().forEach(node -> { + scans.add(((RedisClusterConnection) connection).scan(node, scanOptions)); + }); + } else { + scans.add(connection.scan(scanOptions)); + } + + scans.forEach(scan -> { + scan.forEachRemaining(key -> { + byte[] element = connection.get(key); list.add(deserialize(element)); - } - cursor = res.getCursor(); - } while (!"0".equals(cursor)); + }); + }); return list; } } @@ -90,21 +98,21 @@ public class TbLwM2mRedisSecurityStore implements EditableSecurityStore { @Override public SecurityInfo add(SecurityInfo info) throws NonUniqueSecurityInfoException { byte[] data = serialize(info); - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + try (var connection = connectionFactory.getConnection()) { if (info.getIdentity() != null) { // populate the secondary index (security info by PSK id) - String oldEndpoint = j.hget(PSKID_SEC, info.getIdentity()); - if (oldEndpoint != null && !oldEndpoint.equals(info.getEndpoint())) { + String oldEndpoint = new String(connection.hGet(PSKID_SEC.getBytes(), info.getIdentity().getBytes())); + if (!oldEndpoint.equals(info.getEndpoint())) { throw new NonUniqueSecurityInfoException("PSK Identity " + info.getIdentity() + " is already used"); } - j.hset(PSKID_SEC.getBytes(), info.getIdentity().getBytes(), info.getEndpoint().getBytes()); + connection.hSet(PSKID_SEC.getBytes(), info.getIdentity().getBytes(), info.getEndpoint().getBytes()); } - byte[] previousData = j.getSet((SEC_EP + info.getEndpoint()).getBytes(), data); + byte[] previousData = connection.getSet((SEC_EP + info.getEndpoint()).getBytes(), data); SecurityInfo previous = previousData == null ? null : deserialize(previousData); String previousIdentity = previous == null ? null : previous.getIdentity(); if (previousIdentity != null && !previousIdentity.equals(info.getIdentity())) { - j.hdel(PSKID_SEC, previousIdentity); + connection.hDel(PSKID_SEC.getBytes(), previousIdentity.getBytes()); } return previous; @@ -113,15 +121,15 @@ public class TbLwM2mRedisSecurityStore implements EditableSecurityStore { @Override public SecurityInfo remove(String endpoint, boolean infosAreCompromised) { - try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { - byte[] data = j.get((SEC_EP + endpoint).getBytes()); + try (var connection = connectionFactory.getConnection()) { + byte[] data = connection.get((SEC_EP + endpoint).getBytes()); if (data != null) { SecurityInfo info = deserialize(data); if (info.getIdentity() != null) { - j.hdel(PSKID_SEC.getBytes(), info.getIdentity().getBytes()); + connection.hDel(PSKID_SEC.getBytes(), info.getIdentity().getBytes()); } - j.del((SEC_EP + endpoint).getBytes()); + connection.del((SEC_EP + endpoint).getBytes()); if (listener != null) { listener.securityInfoRemoved(infosAreCompromised, info); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mSecurityStore.java index 512790036f..701d629154 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mSecurityStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mSecurityStore.java @@ -74,7 +74,7 @@ public class TbLwM2mSecurityStore implements EditableSecurityStore { add(securityInfo); } } catch (NonUniqueSecurityInfoException e) { - log.warn("Failed to add security info: {}", securityInfo, e); + log.trace("Failed to add security info: {}", securityInfo, e); } } return securityInfo; @@ -90,7 +90,7 @@ public class TbLwM2mSecurityStore implements EditableSecurityStore { add(securityInfo); } } catch (NonUniqueSecurityInfoException e) { - log.warn("Failed to add security info: {}", securityInfo, e); + log.trace("Failed to add security info: {}", securityInfo, e); } } return securityInfo; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java index 1fcbd15e33..164a78b0a3 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java @@ -26,6 +26,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import java.util.Optional; @@ -37,6 +38,9 @@ public class TbLwM2mStoreFactory { @Autowired(required = false) private Optional redisConfiguration; + @Autowired + private LwM2MTransportServerConfig config; + @Autowired @Lazy private LwM2mClientContext clientContext; @@ -47,7 +51,7 @@ public class TbLwM2mStoreFactory { @Bean private CaliforniumRegistrationStore registrationStore() { return redisConfiguration.isPresent() && useRedis ? - new TbLwM2mRedisRegistrationStore(redisConfiguration.get().redisConnectionFactory()) : new InMemoryRegistrationStore(); + new TbLwM2mRedisRegistrationStore(redisConfiguration.get().redisConnectionFactory()) : new InMemoryRegistrationStore(config.getCleanPeriodInSec()); } @Bean diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java index 77a18af6bc..acc6ea1857 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java @@ -121,9 +121,11 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter { /** let's assume we received an ISO 8601 format date */ try { return new Date(Long.decode(value.toString())); -// DatatypeFactory datatypeFactory = DatatypeFactory.newInstance(); -// XMLGregorianCalendar cal = datatypeFactory.newXMLGregorianCalendar((String) value); -// return cal.toGregorianCalendar().getTime(); + /** + DatatypeFactory datatypeFactory = DatatypeFactory.newInstance(); + XMLGregorianCalendar cal = datatypeFactory.newXMLGregorianCalendar((String) value); + return cal.toGregorianCalendar().getTime(); + **/ } catch (IllegalArgumentException e) { log.debug("Unable to convert string to date", e); throw new CodecException("Unable to convert string (%s) to date for resource %s", value, diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java index ebbfae0f05..eb873db679 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -48,6 +48,8 @@ public interface AlarmDao extends Dao { PageData findAlarms(TenantId tenantId, AlarmQuery query); + PageData findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); + PageData findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, AlarmDataQuery query, Collection orderedEntityIds); diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 9dff668f2e..bb54efe769 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -41,7 +41,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; -import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; @@ -102,6 +101,11 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ @Override public AlarmOperationResult createOrUpdateAlarm(Alarm alarm) { + return createOrUpdateAlarm(alarm, true); + } + + @Override + public AlarmOperationResult createOrUpdateAlarm(Alarm alarm, boolean alarmCreationEnabled) { alarmDataValidator.validate(alarm, Alarm::getTenantId); try { if (alarm.getStartTs() == 0L) { @@ -110,9 +114,13 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ if (alarm.getEndTs() == 0L) { alarm.setEndTs(alarm.getStartTs()); } + alarm.setCustomerId(entityService.fetchEntityCustomerId(alarm.getTenantId(), alarm.getOriginator())); if (alarm.getId() == null) { Alarm existing = alarmDao.findLatestByOriginatorAndType(alarm.getTenantId(), alarm.getOriginator(), alarm.getType()).get(); if (existing == null || existing.getStatus().isCleared()) { + if (!alarmCreationEnabled) { + throw new IllegalStateException("Alarm creation is disabled"); + } return createAlarm(alarm); } else { return updateAlarm(existing, alarm); @@ -158,7 +166,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ log.debug("New Alarm : {}", alarm); Alarm saved = alarmDao.save(alarm.getTenantId(), alarm); List propagatedEntitiesList = createAlarmRelations(saved); - return new AlarmOperationResult(saved, true, propagatedEntitiesList); + return new AlarmOperationResult(saved, true, true, propagatedEntitiesList); } private List createAlarmRelations(Alarm alarm) throws InterruptedException, ExecutionException { @@ -292,25 +300,38 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ public ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query) { PageData alarms = alarmDao.findAlarms(tenantId, query); if (query.getFetchOriginator() != null && query.getFetchOriginator().booleanValue()) { - List> alarmFutures = new ArrayList<>(alarms.getData().size()); - for (AlarmInfo alarmInfo : alarms.getData()) { - alarmFutures.add(Futures.transform( - entityService.fetchEntityNameAsync(tenantId, alarmInfo.getOriginator()), originatorName -> { - if (originatorName == null) { - originatorName = "Deleted"; - } - alarmInfo.setOriginatorName(originatorName); - return alarmInfo; - }, MoreExecutors.directExecutor() - )); - } - return Futures.transform(Futures.successfulAsList(alarmFutures), - alarmInfos -> new PageData<>(alarmInfos, alarms.getTotalPages(), alarms.getTotalElements(), - alarms.hasNext()), MoreExecutors.directExecutor()); + return fetchAlarmsOriginators(tenantId, alarms); } return Futures.immediateFuture(alarms); } + @Override + public ListenableFuture> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) { + PageData alarms = alarmDao.findCustomerAlarms(tenantId, customerId, query); + if (query.getFetchOriginator() != null && query.getFetchOriginator().booleanValue()) { + return fetchAlarmsOriginators(tenantId, alarms); + } + return Futures.immediateFuture(alarms); + } + + private ListenableFuture> fetchAlarmsOriginators(TenantId tenantId, PageData alarms) { + List> alarmFutures = new ArrayList<>(alarms.getData().size()); + for (AlarmInfo alarmInfo : alarms.getData()) { + alarmFutures.add(Futures.transform( + entityService.fetchEntityNameAsync(tenantId, alarmInfo.getOriginator()), originatorName -> { + if (originatorName == null) { + originatorName = "Deleted"; + } + alarmInfo.setOriginatorName(originatorName); + return alarmInfo; + }, MoreExecutors.directExecutor() + )); + } + return Futures.transform(Futures.successfulAsList(alarmFutures), + alarmInfos -> new PageData<>(alarmInfos, alarms.getTotalPages(), alarms.getTotalElements(), + alarms.hasNext()), MoreExecutors.directExecutor()); + } + @Override public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus) { @@ -342,6 +363,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ existing.setStatus(alarm.getStatus()); existing.setSeverity(alarm.getSeverity()); existing.setDetails(alarm.getDetails()); + existing.setCustomerId(alarm.getCustomerId()); existing.setPropagate(existing.isPropagate() || alarm.isPropagate()); List existingPropagateRelationTypes = existing.getPropagateRelationTypes(); List newRelationTypes = alarm.getPropagateRelationTypes(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 97bd5f37a9..d88494c6e1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.asset; -import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -46,11 +45,10 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; @@ -59,8 +57,8 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantDao; -import javax.annotation.Nullable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -180,15 +178,16 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ throw new RuntimeException("Exception while finding entity views for assetId [" + assetId + "]", e); } - List list = new ArrayList<>(); - list.add(asset.getTenantId()); - list.add(asset.getName()); - Cache cache = cacheManager.getCache(ASSET_CACHE); - cache.evict(list); + removeAssetFromCacheByName(asset.getTenantId(), asset.getName()); assetDao.removeById(tenantId, assetId.getId()); } + private void removeAssetFromCacheByName(TenantId tenantId, String name) { + Cache cache = cacheManager.getCache(ASSET_CACHE); + cache.evict(Arrays.asList(tenantId, name)); + } + @Override public PageData findAssetsByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findAssetsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); @@ -397,6 +396,13 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ @Override protected void validateUpdate(TenantId tenantId, Asset asset) { + Asset old = assetDao.findById(asset.getTenantId(), asset.getId().getId()); + if (old == null) { + throw new DataValidationException("Can't update non existing asset!"); + } + if (!old.getName().equals(asset.getName())) { + removeAssetFromCacheByName(tenantId, old.getName()); + } } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 456132a443..7c6150ba3a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.dashboard; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -166,7 +167,16 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb log.trace("Executing deleteDashboard [{}]", dashboardId); Validator.validateId(dashboardId, INCORRECT_DASHBOARD_ID + dashboardId); deleteEntityRelations(tenantId, dashboardId); - dashboardDao.removeById(tenantId, dashboardId.getId()); + try { + dashboardDao.removeById(tenantId, dashboardId.getId()); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_dashboard_device_profile")) { + throw new DataValidationException("The dashboard referenced by the device profiles cannot be deleted!"); + } else { + throw t; + } + } } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 0cc7242a4f..8158134925 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -36,6 +36,7 @@ import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; @@ -61,9 +62,12 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.firmware.FirmwareService; +import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; @@ -117,6 +121,12 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D @Autowired private FirmwareService firmwareService; + @Autowired + private RuleChainService ruleChainService; + + @Autowired + private DashboardService dashboardService; + private final Lock findOrCreateLock = new ReentrantLock(); @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{#deviceProfileId.id}") @@ -336,7 +346,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D } private DataValidator deviceProfileValidator = - new DataValidator() { + new DataValidator<>() { @Override protected void validateDataImpl(TenantId tenantId, DeviceProfile deviceProfile) { if (StringUtils.isEmpty(deviceProfile.getName())) { @@ -402,6 +412,20 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D } } + if (deviceProfile.getDefaultRuleChainId() != null) { + RuleChain ruleChain = ruleChainService.findRuleChainById(tenantId, deviceProfile.getDefaultRuleChainId()); + if (ruleChain == null) { + throw new DataValidationException("Can't assign non-existent rule chain!"); + } + } + + if (deviceProfile.getDefaultDashboardId() != null) { + DashboardInfo dashboard = dashboardService.findDashboardInfoById(tenantId, deviceProfile.getDefaultDashboardId()); + if (dashboard == null) { + throw new DataValidationException("Can't assign non-existent dashboard!"); + } + } + if (deviceProfile.getFirmwareId() != null) { Firmware firmware = firmwareService.findFirmwareById(tenantId, deviceProfile.getFirmwareId()); if (firmware == null) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 937efed0b5..c53c5aef4d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -84,6 +84,7 @@ import org.thingsboard.common.util.JacksonUtil; import javax.annotation.Nullable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -245,7 +246,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_name_unq_key")) { // remove device from cache in case null value cached in the distributed redis. - removeDeviceFromCache(device.getTenantId(), device.getName()); + removeDeviceFromCacheByName(device.getTenantId(), device.getName()); throw new DataValidationException("Device with such name already exists!"); } else { throw t; @@ -322,17 +323,14 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe } deleteEntityRelations(tenantId, deviceId); - removeDeviceFromCache(tenantId, device.getName()); + removeDeviceFromCacheByName(tenantId, device.getName()); deviceDao.removeById(tenantId, deviceId.getId()); } - private void removeDeviceFromCache(TenantId tenantId, String name) { - List list = new ArrayList<>(); - list.add(tenantId); - list.add(name); + private void removeDeviceFromCacheByName(TenantId tenantId, String name) { Cache cache = cacheManager.getCache(DEVICE_CACHE); - cache.evict(list); + cache.evict(Arrays.asList(tenantId, name)); } @Override @@ -671,6 +669,9 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe if (old == null) { throw new DataValidationException("Can't update non existing device!"); } + if (!old.getName().equals(device.getName())) { + removeDeviceFromCacheByName(tenantId, old.getName()); + } } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 8401a43d21..2ff7d94f43 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -58,7 +58,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; @@ -80,6 +79,7 @@ import javax.annotation.PostConstruct; import java.net.InetSocketAddress; import java.net.Proxy; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -222,17 +222,18 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic Edge edge = edgeDao.findById(tenantId, edgeId.getId()); - List list = new ArrayList<>(); - list.add(edge.getTenantId()); - list.add(edge.getName()); - Cache cache = cacheManager.getCache(EDGE_CACHE); - cache.evict(list); - deleteEntityRelations(tenantId, edgeId); + removeEdgeFromCacheByName(edge.getTenantId(), edge.getName()); + edgeDao.removeById(tenantId, edgeId.getId()); } + private void removeEdgeFromCacheByName(TenantId tenantId, String name) { + Cache cache = cacheManager.getCache(EDGE_CACHE); + cache.evict(Arrays.asList(tenantId, name)); + } + @Override public PageData findEdgesByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing findEdgesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); @@ -423,6 +424,10 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic @Override protected void validateUpdate(TenantId tenantId, Edge edge) { + Edge old = edgeDao.findById(edge.getTenantId(), edge.getId().getId()); + if (!old.getName().equals(edge.getName())) { + removeEdgeFromCacheByName(tenantId, old.getName()); + } } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 7e0218740a..73cebc0797 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -22,6 +22,7 @@ import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; @@ -54,6 +55,7 @@ import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; import static org.thingsboard.server.dao.service.Validator.validateId; /** @@ -175,6 +177,50 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe return entityName; } + @Override + public CustomerId fetchEntityCustomerId(TenantId tenantId, EntityId entityId) { + log.trace("Executing fetchEntityCustomerId [{}]", entityId); + HasCustomerId hasCustomerId = null; + switch (entityId.getEntityType()) { + case TENANT: + case RULE_CHAIN: + case RULE_NODE: + case DASHBOARD: + case WIDGETS_BUNDLE: + case WIDGET_TYPE: + case TENANT_PROFILE: + case DEVICE_PROFILE: + case API_USAGE_STATE: + case TB_RESOURCE: + case FIRMWARE: + break; + case CUSTOMER: + hasCustomerId = () -> new CustomerId(entityId.getId()); + break; + case USER: + hasCustomerId = userService.findUserById(tenantId, new UserId(entityId.getId())); + break; + case ASSET: + hasCustomerId = assetService.findAssetById(tenantId, new AssetId(entityId.getId())); + break; + case DEVICE: + hasCustomerId = deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())); + break; + case ALARM: + try { + hasCustomerId = alarmService.findAlarmByIdAsync(tenantId, new AlarmId(entityId.getId())).get(); + } catch (Exception e) {} + break; + case ENTITY_VIEW: + hasCustomerId = entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())); + break; + case EDGE: + hasCustomerId = edgeService.findEdgeById(tenantId, new EdgeId(entityId.getId())); + break; + } + return hasCustomerId != null ? hasCustomerId.getCustomerId() : new CustomerId(NULL_UUID); + } + private static void validateEntityCountQuery(EntityCountQuery query) { if (query == null) { throw new IncorrectParameterException("Query must be specified."); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index bc0afe630d..0bc346e36c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -170,12 +170,14 @@ public class ModelConstants { public static final String DEVICE_PROFILE_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; public static final String DEVICE_PROFILE_NAME_PROPERTY = "name"; public static final String DEVICE_PROFILE_TYPE_PROPERTY = "type"; + public static final String DEVICE_PROFILE_IMAGE_PROPERTY = "image"; public static final String DEVICE_PROFILE_TRANSPORT_TYPE_PROPERTY = "transport_type"; public static final String DEVICE_PROFILE_PROVISION_TYPE_PROPERTY = "provision_type"; public static final String DEVICE_PROFILE_PROFILE_DATA_PROPERTY = "profile_data"; public static final String DEVICE_PROFILE_DESCRIPTION_PROPERTY = "description"; public static final String DEVICE_PROFILE_IS_DEFAULT_PROPERTY = "is_default"; public static final String DEVICE_PROFILE_DEFAULT_RULE_CHAIN_ID_PROPERTY = "default_rule_chain_id"; + public static final String DEVICE_PROFILE_DEFAULT_DASHBOARD_ID_PROPERTY = "default_dashboard_id"; public static final String DEVICE_PROFILE_DEFAULT_QUEUE_NAME_PROPERTY = "default_queue_name"; public static final String DEVICE_PROFILE_PROVISION_DEVICE_KEY = "provision_device_key"; public static final String DEVICE_PROFILE_FIRMWARE_ID_PROPERTY = "firmware_id"; @@ -257,6 +259,7 @@ public class ModelConstants { */ public static final String ALARM_COLUMN_FAMILY_NAME = "alarm"; public static final String ALARM_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String ALARM_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; public static final String ALARM_TYPE_PROPERTY = "type"; public static final String ALARM_DETAILS_PROPERTY = "details"; public static final String ALARM_ORIGINATOR_ID_PROPERTY = "originator_id"; @@ -333,6 +336,7 @@ public class ModelConstants { public static final String DASHBOARD_COLUMN_FAMILY_NAME = "dashboard"; public static final String DASHBOARD_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; public static final String DASHBOARD_TITLE_PROPERTY = TITLE_PROPERTY; + public static final String DASHBOARD_IMAGE_PROPERTY = "image"; public static final String DASHBOARD_CONFIGURATION_PROPERTY = "configuration"; public static final String DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY = "assigned_customers"; @@ -461,6 +465,7 @@ public class ModelConstants { public static final String API_USAGE_STATE_JS_EXEC_COLUMN = "js_exec"; public static final String API_USAGE_STATE_EMAIL_EXEC_COLUMN = "email_exec"; public static final String API_USAGE_STATE_SMS_EXEC_COLUMN = "sms_exec"; + public static final String API_USAGE_STATE_ALARM_EXEC_COLUMN = "alarm_exec"; /** * Resource constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java index 297fa39e62..8ade2b78d5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseEntity; @@ -44,6 +45,7 @@ import java.util.UUID; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ACK_TS_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CLEAR_TS_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CUSTOMER_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_END_TS_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY; @@ -64,6 +66,9 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity @Column(name = ALARM_TENANT_ID_PROPERTY) private UUID tenantId; + @Column(name = ALARM_CUSTOMER_ID_PROPERTY) + private UUID customerId; + @Column(name = ALARM_ORIGINATOR_ID_PROPERTY) private UUID originatorId; @@ -115,6 +120,9 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity if (alarm.getTenantId() != null) { this.tenantId = alarm.getTenantId().getId(); } + if (alarm.getCustomerId() != null) { + this.customerId = alarm.getCustomerId().getId(); + } this.type = alarm.getType(); this.originatorId = alarm.getOriginator().getId(); this.originatorType = alarm.getOriginator().getEntityType(); @@ -138,6 +146,7 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity this.setId(alarmEntity.getId()); this.setCreatedTime(alarmEntity.getCreatedTime()); this.tenantId = alarmEntity.getTenantId(); + this.customerId = alarmEntity.getCustomerId(); this.type = alarmEntity.getType(); this.originatorId = alarmEntity.getOriginatorId(); this.originatorType = alarmEntity.getOriginatorType(); @@ -159,6 +168,9 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity if (tenantId != null) { alarm.setTenantId(new TenantId(tenantId)); } + if (customerId != null) { + alarm.setCustomerId(new CustomerId(customerId)); + } alarm.setOriginator(EntityIdFactory.getByTypeAndUuid(originatorType, originatorId)); alarm.setType(type); alarm.setSeverity(severity); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java index 73b181a170..5848ffa572 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmEntity.java @@ -19,6 +19,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Entity; @@ -37,6 +38,10 @@ public final class AlarmEntity extends AbstractAlarmEntity { super(); } + public AlarmEntity(AlarmInfo alarmInfo) { + super(alarmInfo); + } + public AlarmEntity(Alarm alarm) { super(alarm); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java index fd069819a2..40b56c0095 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java @@ -69,6 +69,9 @@ public class ApiUsageStateEntity extends BaseSqlEntity implements @Enumerated(EnumType.STRING) @Column(name = ModelConstants.API_USAGE_STATE_SMS_EXEC_COLUMN) private ApiUsageStateValue smsExecState = ApiUsageStateValue.ENABLED; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.API_USAGE_STATE_ALARM_EXEC_COLUMN) + private ApiUsageStateValue alarmExecState = ApiUsageStateValue.ENABLED; public ApiUsageStateEntity() { } @@ -91,6 +94,7 @@ public class ApiUsageStateEntity extends BaseSqlEntity implements this.jsExecState = ur.getJsExecState(); this.emailExecState = ur.getEmailExecState(); this.smsExecState = ur.getSmsExecState(); + this.alarmExecState = ur.getAlarmExecState(); } @Override @@ -109,6 +113,7 @@ public class ApiUsageStateEntity extends BaseSqlEntity implements ur.setJsExecState(jsExecState); ur.setEmailExecState(emailExecState); ur.setSmsExecState(smsExecState); + ur.setAlarmExecState(alarmExecState); return ur; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java index e0780a5fc9..8e499858bc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java @@ -58,7 +58,10 @@ public final class DashboardEntity extends BaseSqlEntity implements S @Column(name = ModelConstants.DASHBOARD_TITLE_PROPERTY) private String title; - + + @Column(name = ModelConstants.DASHBOARD_IMAGE_PROPERTY) + private String image; + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) private String searchText; @@ -82,6 +85,7 @@ public final class DashboardEntity extends BaseSqlEntity implements S this.tenantId = dashboard.getTenantId().getId(); } this.title = dashboard.getTitle(); + this.image = dashboard.getImage(); if (dashboard.getAssignedCustomers() != null) { try { this.assignedCustomers = objectMapper.writeValueAsString(dashboard.getAssignedCustomers()); @@ -110,6 +114,7 @@ public final class DashboardEntity extends BaseSqlEntity implements S dashboard.setTenantId(new TenantId(tenantId)); } dashboard.setTitle(title); + dashboard.setImage(image); if (!StringUtils.isEmpty(assignedCustomers)) { try { dashboard.setAssignedCustomers(objectMapper.readValue(assignedCustomers, assignedCustomersType)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java index 47c7016aba..b3d7738843 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java @@ -54,6 +54,9 @@ public class DashboardInfoEntity extends BaseSqlEntity implements @Column(name = ModelConstants.DASHBOARD_TITLE_PROPERTY) private String title; + @Column(name = ModelConstants.DASHBOARD_IMAGE_PROPERTY) + private String image; + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) private String searchText; @@ -73,6 +76,7 @@ public class DashboardInfoEntity extends BaseSqlEntity implements this.tenantId = dashboardInfo.getTenantId().getId(); } this.title = dashboardInfo.getTitle(); + this.image = dashboardInfo.getImage(); if (dashboardInfo.getAssignedCustomers() != null) { try { this.assignedCustomers = objectMapper.writeValueAsString(dashboardInfo.getAssignedCustomers()); @@ -104,6 +108,7 @@ public class DashboardInfoEntity extends BaseSqlEntity implements dashboardInfo.setTenantId(new TenantId(tenantId)); } dashboardInfo.setTitle(title); + dashboardInfo.setImage(image); if (!StringUtils.isEmpty(assignedCustomers)) { try { dashboardInfo.setAssignedCustomers(objectMapper.readValue(assignedCustomers, assignedCustomersType)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java index e4c32a44f0..f2124ecbab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.FirmwareId; import org.thingsboard.server.common.data.id.RuleChainId; @@ -60,6 +61,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl @Column(name = ModelConstants.DEVICE_PROFILE_TYPE_PROPERTY) private DeviceProfileType type; + @Column(name = ModelConstants.DEVICE_PROFILE_IMAGE_PROPERTY) + private String image; + @Enumerated(EnumType.STRING) @Column(name = ModelConstants.DEVICE_PROFILE_TRANSPORT_TYPE_PROPERTY) private DeviceTransportType transportType; @@ -80,6 +84,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl @Column(name = ModelConstants.DEVICE_PROFILE_DEFAULT_RULE_CHAIN_ID_PROPERTY, columnDefinition = "uuid") private UUID defaultRuleChainId; + @Column(name = ModelConstants.DEVICE_PROFILE_DEFAULT_DASHBOARD_ID_PROPERTY) + private UUID defaultDashboardId; + @Column(name = ModelConstants.DEVICE_PROFILE_DEFAULT_QUEUE_NAME_PROPERTY) private String defaultQueueName; @@ -124,7 +131,7 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl this.firmwareId = deviceProfile.getFirmwareId().getId(); } if (deviceProfile.getSoftwareId() != null) { - this.firmwareId = deviceProfile.getSoftwareId().getId(); + this.softwareId = deviceProfile.getSoftwareId().getId(); } } @@ -151,6 +158,7 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl } deviceProfile.setName(name); deviceProfile.setType(type); + deviceProfile.setImage(image); deviceProfile.setTransportType(transportType); deviceProfile.setProvisionType(provisionType); deviceProfile.setDescription(description); @@ -159,6 +167,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl if (defaultRuleChainId != null) { deviceProfile.setDefaultRuleChainId(new RuleChainId(defaultRuleChainId)); } + if (defaultDashboardId != null) { + deviceProfile.setDefaultDashboardId(new DashboardId(defaultDashboardId)); + } deviceProfile.setDefaultQueueName(defaultQueueName); deviceProfile.setProvisionDeviceKey(provisionDeviceKey); diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 786d86d8dd..b61e18e08a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -100,12 +100,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Transactional public RuleChain saveRuleChain(RuleChain ruleChain) { ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); - RuleChain savedRuleChain = ruleChainDao.save(ruleChain.getTenantId(), ruleChain); - if (ruleChain.isRoot() && ruleChain.getId() == null) { - createRelation(ruleChain.getTenantId(), new EntityRelation(savedRuleChain.getTenantId(), savedRuleChain.getId(), - EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); - } - return savedRuleChain; + return ruleChainDao.save(ruleChain.getTenantId(), ruleChain); } @Override @@ -114,29 +109,20 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC RuleChain ruleChain = ruleChainDao.findById(tenantId, ruleChainId.getId()); if (!ruleChain.isRoot()) { RuleChain previousRootRuleChain = getRootTenantRuleChain(ruleChain.getTenantId()); - try { - if (previousRootRuleChain == null) { - setRootAndSave(tenantId, ruleChain); - return true; - } else if (!previousRootRuleChain.getId().equals(ruleChain.getId())) { - deleteRelation(tenantId, new EntityRelation(previousRootRuleChain.getTenantId(), previousRootRuleChain.getId(), - EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); - previousRootRuleChain.setRoot(false); - ruleChainDao.save(tenantId, previousRootRuleChain); - setRootAndSave(tenantId, ruleChain); - return true; - } - } catch (ExecutionException | InterruptedException e) { - log.warn("[{}] Failed to set root rule chain, ruleChainId: [{}]", ruleChainId); - throw new RuntimeException(e); + if (previousRootRuleChain == null) { + setRootAndSave(tenantId, ruleChain); + return true; + } else if (!previousRootRuleChain.getId().equals(ruleChain.getId())) { + previousRootRuleChain.setRoot(false); + ruleChainDao.save(tenantId, previousRootRuleChain); + setRootAndSave(tenantId, ruleChain); + return true; } } return false; } - private void setRootAndSave(TenantId tenantId, RuleChain ruleChain) throws ExecutionException, InterruptedException { - createRelation(tenantId, new EntityRelation(ruleChain.getTenantId(), ruleChain.getId(), - EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); + private void setRootAndSave(TenantId tenantId, RuleChain ruleChain) { ruleChain.setRoot(true); ruleChainDao.save(tenantId, ruleChain); } @@ -179,41 +165,45 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC toDelete.add(existingNode); } } - for (RuleNode node : toAddOrUpdate) { - node.setRuleChainId(ruleChain.getId()); - RuleNode savedNode = ruleNodeDao.save(tenantId, node); - createRelation(tenantId, new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(), - EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); - int index = nodes.indexOf(node); - nodes.set(index, savedNode); - ruleNodeIndexMap.put(savedNode.getId(), index); + if (nodes != null) { + for (RuleNode node : toAddOrUpdate) { + node.setRuleChainId(ruleChain.getId()); + RuleNode savedNode = ruleNodeDao.save(tenantId, node); + createRelation(tenantId, new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(), + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); + int index = nodes.indexOf(node); + nodes.set(index, savedNode); + ruleNodeIndexMap.put(savedNode.getId(), index); + } } for (RuleNode node : toDelete) { deleteRuleNode(tenantId, node.getId()); } RuleNodeId firstRuleNodeId = null; - if (ruleChainMetaData.getFirstNodeIndex() != null) { - firstRuleNodeId = nodes.get(ruleChainMetaData.getFirstNodeIndex()).getId(); - } - if ((ruleChain.getFirstRuleNodeId() != null && !ruleChain.getFirstRuleNodeId().equals(firstRuleNodeId)) - || (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) { - ruleChain.setFirstRuleNodeId(firstRuleNodeId); - ruleChainDao.save(tenantId, ruleChain); - } - if (ruleChainMetaData.getConnections() != null) { - for (NodeConnectionInfo nodeConnection : ruleChainMetaData.getConnections()) { - EntityId from = nodes.get(nodeConnection.getFromIndex()).getId(); - EntityId to = nodes.get(nodeConnection.getToIndex()).getId(); - String type = nodeConnection.getType(); - createRelation(tenantId, new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE)); + if (nodes != null) { + if (ruleChainMetaData.getFirstNodeIndex() != null) { + firstRuleNodeId = nodes.get(ruleChainMetaData.getFirstNodeIndex()).getId(); } - } - if (ruleChainMetaData.getRuleChainConnections() != null) { - for (RuleChainConnectionInfo nodeToRuleChainConnection : ruleChainMetaData.getRuleChainConnections()) { - EntityId from = nodes.get(nodeToRuleChainConnection.getFromIndex()).getId(); - EntityId to = nodeToRuleChainConnection.getTargetRuleChainId(); - String type = nodeToRuleChainConnection.getType(); - createRelation(tenantId, new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo())); + if ((ruleChain.getFirstRuleNodeId() != null && !ruleChain.getFirstRuleNodeId().equals(firstRuleNodeId)) + || (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) { + ruleChain.setFirstRuleNodeId(firstRuleNodeId); + ruleChainDao.save(tenantId, ruleChain); + } + if (ruleChainMetaData.getConnections() != null) { + for (NodeConnectionInfo nodeConnection : ruleChainMetaData.getConnections()) { + EntityId from = nodes.get(nodeConnection.getFromIndex()).getId(); + EntityId to = nodes.get(nodeConnection.getToIndex()).getId(); + String type = nodeConnection.getType(); + createRelation(tenantId, new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE)); + } + } + if (ruleChainMetaData.getRuleChainConnections() != null) { + for (RuleChainConnectionInfo nodeToRuleChainConnection : ruleChainMetaData.getRuleChainConnections()) { + EntityId from = nodes.get(nodeToRuleChainConnection.getFromIndex()).getId(); + EntityId to = nodeToRuleChainConnection.getTargetRuleChainId(); + String type = nodeToRuleChainConnection.getType(); + createRelation(tenantId, new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo())); + } } } @@ -307,24 +297,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override public RuleChain getRootTenantRuleChain(TenantId tenantId) { - return getRootRuleChainByType(tenantId, RuleChainType.CORE); - } - - private RuleChain getRootRuleChainByType(TenantId tenantId, RuleChainType type) { Validator.validateId(tenantId, "Incorrect tenant id for search request."); - List relations = relationService.findByFrom(tenantId, tenantId, RelationTypeGroup.RULE_CHAIN); - if (relations != null && !relations.isEmpty()) { - for (EntityRelation relation : relations) { - RuleChainId ruleChainId = new RuleChainId(relation.getTo().getId()); - RuleChain ruleChainById = findRuleChainById(tenantId, ruleChainId); - if (type.equals(ruleChainById.getType())) { - return ruleChainById; - } - } - return null; - } else { - return null; - } + return ruleChainDao.findRootRuleChainByTenantIdAndType(tenantId.getId(), RuleChainType.CORE); } @Override @@ -366,7 +340,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC boolean valid = true; EntityType toType = relation.getTo().getEntityType(); if (toType == EntityType.RULE_NODE || toType == EntityType.RULE_CHAIN) { - BaseData entity; + BaseData entity; if (relation.getTo().getEntityType() == EntityType.RULE_NODE) { entity = ruleNodeDao.findById(tenantId, relation.getTo().getId()); } else { @@ -501,9 +475,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC ObjectNode objNode = (ObjectNode) node; objNode.put("id", tenantId.getId().toString()); } else { - Iterator childIter = node.iterator(); - while (childIter.hasNext()) { - searchTenantIdRecursive(tenantId, childIter.next()); + for (JsonNode jsonNode : node) { + searchTenantIdRecursive(tenantId, jsonNode); } } } @@ -608,7 +581,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override public RuleChain getEdgeTemplateRootRuleChain(TenantId tenantId) { - return getRootRuleChainByType(tenantId, RuleChainType.EDGE); + Validator.validateId(tenantId, "Incorrect tenant id for search request."); + return ruleChainDao.findRootRuleChainByTenantIdAndType(tenantId.getId(), RuleChainType.EDGE); } @Override @@ -618,13 +592,9 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (previousEdgeTemplateRootRuleChain == null || !previousEdgeTemplateRootRuleChain.getId().equals(ruleChain.getId())) { try { if (previousEdgeTemplateRootRuleChain != null) { - deleteRelation(tenantId, new EntityRelation(previousEdgeTemplateRootRuleChain.getTenantId(), previousEdgeTemplateRootRuleChain.getId(), - EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); previousEdgeTemplateRootRuleChain.setRoot(false); ruleChainDao.save(tenantId, previousEdgeTemplateRootRuleChain); } - createRelation(tenantId, new EntityRelation(ruleChain.getTenantId(), ruleChain.getId(), - EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); ruleChain.setRoot(true); ruleChainDao.save(tenantId, ruleChain); return true; @@ -699,8 +669,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC ruleNodeDao.removeById(tenantId, entityId.getId()); } - private DataValidator ruleChainValidator = - new DataValidator() { + private final DataValidator ruleChainValidator = + new DataValidator<>() { @Override protected void validateCreate(TenantId tenantId, RuleChain data) { DefaultTenantProfileConfiguration profileConfiguration = @@ -739,8 +709,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } }; - private PaginatedRemover tenantRuleChainsRemover = - new PaginatedRemover() { + private final PaginatedRemover tenantRuleChainsRemover = + new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java index 4baa3bd576..2aaaa934d7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java @@ -50,6 +50,15 @@ public interface RuleChainDao extends Dao, TenantEntityDao { */ PageData findRuleChainsByTenantIdAndType(UUID tenantId, RuleChainType type, PageLink pageLink); + /** + * Find root rule chain by tenantId and type + * + * @param tenantId the tenantId + * @param type the type + * @return the rule chain object + */ + RuleChain findRootRuleChainByTenantIdAndType(UUID tenantId, RuleChainType type); + /** * Find rule chains by tenantId, edgeId and page link. * diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index 446125e4fb..b4c0ac09c6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -94,6 +94,58 @@ public interface AlarmRepository extends CrudRepository { @Param("searchText") String searchText, Pageable pageable); + @Query(value = "SELECT new org.thingsboard.server.dao.model.sql.AlarmInfoEntity(a) FROM AlarmEntity a " + + "WHERE a.tenantId = :tenantId " + + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) ", + countQuery = "" + + "SELECT count(a) " + + "FROM AlarmEntity a " + + "WHERE a.tenantId = :tenantId " + + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) ") + Page findAllAlarms(@Param("tenantId") UUID tenantId, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + @Param("alarmStatuses") Set alarmStatuses, + @Param("searchText") String searchText, + Pageable pageable); + + @Query(value = "SELECT new org.thingsboard.server.dao.model.sql.AlarmInfoEntity(a) FROM AlarmEntity a " + + "WHERE a.tenantId = :tenantId AND a.customerId = :customerId " + + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) " + , + countQuery = "" + + "SELECT count(a) " + + "FROM AlarmEntity a " + + "WHERE a.tenantId = :tenantId AND a.customerId = :customerId " + + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) ") + Page findCustomerAlarms(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + @Param("alarmStatuses") Set alarmStatuses, + @Param("searchText") String searchText, + Pageable pageable); + @Query(value = "SELECT a.severity FROM AlarmEntity a " + "LEFT JOIN RelationEntity re ON a.id = re.toId " + "AND re.relationTypeGroup = 'ALARM' " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index bfca088c69..cf222413b0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -103,11 +103,46 @@ public class JpaAlarmDao extends JpaAbstractDao implements A } else if (query.getStatus() != null) { statusSet = Collections.singleton(query.getStatus()); } + if (affectedEntity != null) { + return DaoUtil.toPageData( + alarmRepository.findAlarms( + tenantId.getId(), + affectedEntity.getId(), + affectedEntity.getEntityType().name(), + query.getPageLink().getStartTime(), + query.getPageLink().getEndTime(), + statusSet, + Objects.toString(query.getPageLink().getTextSearch(), ""), + DaoUtil.toPageable(query.getPageLink()) + ) + ); + } else { + return DaoUtil.toPageData( + alarmRepository.findAllAlarms( + tenantId.getId(), + query.getPageLink().getStartTime(), + query.getPageLink().getEndTime(), + statusSet, + Objects.toString(query.getPageLink().getTextSearch(), ""), + DaoUtil.toPageable(query.getPageLink()) + ) + ); + } + } + + @Override + public PageData findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) { + log.trace("Try to find customer alarms by status [{}] and pageLink [{}]", query.getStatus(), query.getPageLink()); + Set statusSet = null; + if (query.getSearchStatus() != null) { + statusSet = query.getSearchStatus().getStatuses(); + } else if (query.getStatus() != null) { + statusSet = Collections.singleton(query.getStatus()); + } return DaoUtil.toPageData( - alarmRepository.findAlarms( + alarmRepository.findCustomerAlarms( tenantId.getId(), - affectedEntity.getId(), - affectedEntity.getEntityType().name(), + customerId.getId(), query.getPageLink().getStartTime(), query.getPageLink().getEndTime(), statusSet, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java index c4d6db691b..1dc2af4ecf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java @@ -28,7 +28,7 @@ import java.util.UUID; public interface DeviceProfileRepository extends PagingAndSortingRepository { - @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.type, d.transportType) " + + @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " + "FROM DeviceProfileEntity d " + "WHERE d.id = :deviceProfileId") DeviceProfileInfo findDeviceProfileInfoById(@Param("deviceProfileId") UUID deviceProfileId); @@ -39,14 +39,14 @@ public interface DeviceProfileRepository extends PagingAndSortingRepository findDeviceProfileInfos(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); - @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.type, d.transportType) " + + @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " + "FROM DeviceProfileEntity d WHERE " + "d.tenantId = :tenantId AND d.transportType = :transportType AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") Page findDeviceProfileInfos(@Param("tenantId") UUID tenantId, @@ -58,7 +58,7 @@ public interface DeviceProfileRepository extends PagingAndSortingRepository findRuleChainsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink) { log.debug("Try to find rule chains by tenantId [{}], edgeId [{}] and pageLink [{}]", tenantId, edgeId, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java index 3c3c5ff3b4..1251234fa1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java @@ -50,5 +50,8 @@ public interface RuleChainRepository extends PagingAndSortingRepository deviceProfileInfos = deviceProfiles.stream() .map(deviceProfile -> new DeviceProfileInfo(deviceProfile.getId(), - deviceProfile.getName(), deviceProfile.getType(), deviceProfile.getTransportType())).collect(Collectors.toList()); + deviceProfile.getName(), deviceProfile.getImage(), deviceProfile.getDefaultDashboardId(), + deviceProfile.getType(), deviceProfile.getTransportType())).collect(Collectors.toList()); Assert.assertEquals(deviceProfileInfos, loadedDeviceProfileInfos); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java index 4fcdd93bce..7567e6b6d8 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java @@ -793,4 +793,25 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { customerService.deleteCustomer(tenantId, customerId); } + @Test + public void testCleanCacheIfDeviceRenamed() { + String deviceNameBeforeRename = RandomStringUtils.randomAlphanumeric(15); + String deviceNameAfterRename = RandomStringUtils.randomAlphanumeric(15); + + Device device = new Device(); + device.setTenantId(tenantId); + device.setName(deviceNameBeforeRename); + device.setType("default"); + deviceService.saveDevice(device); + + Device savedDevice = deviceService.findDeviceByTenantIdAndName(tenantId, deviceNameBeforeRename); + + savedDevice.setName(deviceNameAfterRename); + deviceService.saveDevice(savedDevice); + + Device renamedDevice = deviceService.findDeviceByTenantIdAndName(tenantId, deviceNameBeforeRename); + + Assert.assertNull("Can't find device by name in cache if it was renamed", renamedDevice); + deviceService.deleteDevice(tenantId, savedDevice.getId()); + } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java index 4725b52e6f..e123039cc5 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java @@ -595,4 +595,23 @@ public abstract class BaseEdgeServiceTest extends AbstractServiceTest { return edge; } + @Test + public void testCleanCacheIfEdgeRenamed() { + String edgeNameBeforeRename = RandomStringUtils.randomAlphanumeric(15); + String edgeNameAfterRename = RandomStringUtils.randomAlphanumeric(15); + + Edge edge = constructEdge(tenantId, edgeNameBeforeRename, "default"); + edgeService.saveEdge(edge); + + Edge savedEdge = edgeService.findEdgeByTenantIdAndName(tenantId, edgeNameBeforeRename); + + savedEdge.setName(edgeNameAfterRename); + edgeService.saveEdge(savedEdge); + + Edge renamedEdge = edgeService.findEdgeByTenantIdAndName(tenantId, edgeNameBeforeRename); + + Assert.assertNull("Can't find edge by name in cache if it was renamed", renamedEdge); + edgeService.deleteEdge(tenantId, savedEdge.getId()); + } + } \ No newline at end of file diff --git a/dao/src/test/resources/sql/hsql/drop-all-tables.sql b/dao/src/test/resources/sql/hsql/drop-all-tables.sql index 09b7221cf4..f65316f2c1 100644 --- a/dao/src/test/resources/sql/hsql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/hsql/drop-all-tables.sql @@ -5,7 +5,6 @@ DROP TABLE IF EXISTS audit_log; DROP TABLE IF EXISTS attribute_kv; DROP TABLE IF EXISTS component_descriptor; DROP TABLE IF EXISTS customer; -DROP TABLE IF EXISTS dashboard; DROP TABLE IF EXISTS device; DROP TABLE IF EXISTS device_credentials; DROP TABLE IF EXISTS event; @@ -21,6 +20,7 @@ DROP TABLE IF EXISTS widgets_bundle; DROP TABLE IF EXISTS entity_view; DROP TABLE IF EXISTS device_profile; DROP TABLE IF EXISTS tenant_profile; +DROP TABLE IF EXISTS dashboard; DROP TABLE IF EXISTS rule_node_state; DROP TABLE IF EXISTS rule_node; DROP TABLE IF EXISTS rule_chain; diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index 57661fb036..855a53df2d 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -5,7 +5,6 @@ DROP TABLE IF EXISTS audit_log; DROP TABLE IF EXISTS attribute_kv; DROP TABLE IF EXISTS component_descriptor; DROP TABLE IF EXISTS customer; -DROP TABLE IF EXISTS dashboard; DROP TABLE IF EXISTS device; DROP TABLE IF EXISTS device_credentials; DROP TABLE IF EXISTS event; @@ -21,6 +20,7 @@ DROP TABLE IF EXISTS widgets_bundle; DROP TABLE IF EXISTS entity_view; DROP TABLE IF EXISTS device_profile; DROP TABLE IF EXISTS tenant_profile; +DROP TABLE IF EXISTS dashboard; DROP TABLE IF EXISTS rule_node_state; DROP TABLE IF EXISTS rule_node; DROP TABLE IF EXISTS rule_chain; diff --git a/dao/src/test/resources/sql/timescale/drop-all-tables.sql b/dao/src/test/resources/sql/timescale/drop-all-tables.sql index 430635a758..53da64aff2 100644 --- a/dao/src/test/resources/sql/timescale/drop-all-tables.sql +++ b/dao/src/test/resources/sql/timescale/drop-all-tables.sql @@ -5,7 +5,6 @@ DROP TABLE IF EXISTS audit_log; DROP TABLE IF EXISTS attribute_kv; DROP TABLE IF EXISTS component_descriptor; DROP TABLE IF EXISTS customer; -DROP TABLE IF EXISTS dashboard; DROP TABLE IF EXISTS device; DROP TABLE IF EXISTS device_credentials; DROP TABLE IF EXISTS event; @@ -24,6 +23,7 @@ DROP TABLE IF EXISTS rule_chain; DROP TABLE IF EXISTS entity_view; DROP TABLE IF EXISTS device_profile; DROP TABLE IF EXISTS tenant_profile; +DROP TABLE IF EXISTS dashboard; DROP TABLE IF EXISTS edge; DROP TABLE IF EXISTS edge_event; DROP TABLE IF EXISTS tb_schema_settings; diff --git a/pom.xml b/pom.xml index 4073fe824e..29a373f2e4 100755 --- a/pom.xml +++ b/pom.xml @@ -1430,6 +1430,11 @@ spring-data-redis ${spring-data-redis.version} + + org.springframework.integration + spring-integration-redis + ${spring.version} + redis.clients jedis diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java index d6a0a3e576..7d3157588b 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java @@ -57,6 +57,8 @@ public interface RuleEngineAlarmService { ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query); + ListenableFuture> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); + AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus); PageData findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, AlarmDataQuery query, Collection orderedEntityIds); diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 6fa494f61b..39acda4590 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -101,38 +101,14 @@ transport: lwm2m: # Enable/disable lvm2m transport protocol. enabled: "${LWM2M_ENABLED:true}" - # We choose a default timeout a bit higher to the MAX_TRANSMIT_WAIT(62-93s) which is the time from starting to - # send a Confirmable message to the time when an acknowledgement is no longer expected. - # DEFAULT_TIMEOUT = 2 * 60 * 1000l; 2 min in ms - timeout: "${LWM2M_TIMEOUT:120000}" - recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}" - recommended_supported_groups: "${LWM2M_RECOMMENDED_SUPPORTED_GROUPS:true}" - response_pool_size: "${LWM2M_RESPONSE_POOL_SIZE:100}" - registered_pool_size: "${LWM2M_REGISTERED_POOL_SIZE:10}" - update_registered_pool_size: "${LWM2M_UPDATE_REGISTERED_POOL_SIZE:10}" - un_registered_pool_size: "${LWM2M_UN_REGISTERED_POOL_SIZE:10}" - secure: - # Certificate_x509: - # To get helps about files format and how to generate it, see: https://github.com/eclipse/leshan/wiki/Credential-files-format - # Create new X509 Certificates: common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh - key_store_type: "${LWM2M_KEYSTORE_TYPE:JKS}" - # key_store_path_file: "${KEY_STORE_PATH_FILE:/transport/lwm2m/src/main/data/credentials/serverKeyStore.jks}" - key_store_path_file: "${KEY_STORE_PATH_FILE:}" - key_store_password: "${LWM2M_KEYSTORE_PASSWORD_SERVER:server_ks_password}" - root_alias: "${LWM2M_SERVER_ROOT_CA:rootca}" - enable_gen_new_key_psk_rpk: "${ENABLE_GEN_NEW_KEY_PSK_RPK:false}" server: id: "${LWM2M_SERVER_ID:123}" bind_address: "${LWM2M_BIND_ADDRESS:0.0.0.0}" - bind_port_no_sec: "${LWM2M_BIND_PORT_NO_SEC:5685}" - secure: - bind_address_security: "${LWM2M_BIND_ADDRESS_SECURITY:0.0.0.0}" - bind_port_security: "${LWM2M_BIND_PORT_SECURITY:5686}" - # create_rpk: "${CREATE_RPK:}" + bind_port: "${LWM2M_BIND_PORT:5685}" + security: + bind_address: "${LWM2M_BIND_ADDRESS_SECURITY:0.0.0.0}" + bind_port: "${LWM2M_BIND_PORT_SECURITY:5686}" # Only for RPK: Public & Private Key. If the keystore file is missing or not working - # - Public Key (Hex): [3059301306072a8648ce3d020106082a8648ce3d0301070342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b] - # - Private Key (Hex): [308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b], - # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)] public_x: "${LWM2M_SERVER_PUBLIC_X:05064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f358}" public_y: "${LWM2M_SERVER_PUBLIC_Y:5eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED:308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" @@ -142,19 +118,36 @@ transport: enable: "${LWM2M_ENABLED_BS:true}" id: "${LWM2M_SERVER_ID_BS:111}" bind_address: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" - bind_port_no_sec: "${LWM2M_BIND_PORT_NO_SEC_BS:5687}" - secure: - bind_address_security: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" - bind_port_security: "${LWM2M_BIND_PORT_SECURITY_BS:5688}" + bind_port: "${LWM2M_BIND_PORT_BS:5687}" + security: + bind_address: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" + bind_port: "${LWM2M_BIND_PORT_SECURITY_BS:5688}" # Only for RPK: Public & Private Key. If the keystore file is missing or not working - # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)] - # - Public Key (Hex): [3059301306072a8648ce3d020106082a8648ce3d030107034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34] - # - Private Key (Hex): [308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34], public_x: "${LWM2M_SERVER_PUBLIC_X_BS:5017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f91}" public_y: "${LWM2M_SERVER_PUBLIC_Y_BS:3fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED_BS:308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" # Only Certificate_x509: alias: "${LWM2M_KEYSTORE_ALIAS_BS:bootstrap}" + security: + # Certificate_x509: + # To get helps about files format and how to generate it, see: https://github.com/eclipse/leshan/wiki/Credential-files-format + # Create new X509 Certificates: common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh + key_store_type: "${LWM2M_KEYSTORE_TYPE:JKS}" + # key_store_path_file: "${KEY_STORE_PATH_FILE:/common/transport/lwm2m/src/main/resources/credentials/serverKeyStore.jks" + key_store: "${LWM2M_KEY_STORE:lwm2mserver.jks}" + key_store_password: "${LWM2M_KEY_STORE_PASSWORD:server_ks_password}" + root_alias: "${LWM2M_SERVER_ROOT_CA:rootca}" + enable_gen_new_key_psk_rpk: "${ENABLE_GEN_NEW_KEY_PSK_RPK:false}" + timeout: "${LWM2M_TIMEOUT:120000}" + recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}" + recommended_supported_groups: "${LWM2M_RECOMMENDED_SUPPORTED_GROUPS:true}" + response_pool_size: "${LWM2M_RESPONSE_POOL_SIZE:100}" + registered_pool_size: "${LWM2M_REGISTERED_POOL_SIZE:10}" + registration_store_pool_size: "${LWM2M_REGISTRATION_STORE_POOL_SIZE:100}" + clean_period_in_sec: "${LWM2M_CLEAN_PERIOD_IN_SEC:2}" + update_registered_pool_size: "${LWM2M_UPDATE_REGISTERED_POOL_SIZE:10}" + un_registered_pool_size: "${LWM2M_UN_REGISTERED_POOL_SIZE:10}" + log_max_length: "${LWM2M_LOG_MAX_LENGTH:100}" # Use redis for Security and Registration stores redis.enabled: "${LWM2M_REDIS_ENABLED:false}" diff --git a/transport/snmp/src/main/java/org/thingsboard/server/snmp/ThingsboardSnmpTransportApplication.java b/transport/snmp/src/main/java/org/thingsboard/server/snmp/ThingsboardSnmpTransportApplication.java index 47d96db8c5..2829764059 100644 --- a/transport/snmp/src/main/java/org/thingsboard/server/snmp/ThingsboardSnmpTransportApplication.java +++ b/transport/snmp/src/main/java/org/thingsboard/server/snmp/ThingsboardSnmpTransportApplication.java @@ -26,7 +26,7 @@ import java.util.Arrays; @SpringBootConfiguration @EnableAsync @EnableScheduling -@ComponentScan({"org.thingsboard.server.snmp", "org.thingsboard.server.common", "org.thingsboard.server.transport.snmp", "org.thingsboard.server.queue"}) +@ComponentScan({"org.thingsboard.server.snmp", "org.thingsboard.server.common", "org.thingsboard.server.transport.snmp", "org.thingsboard.server.queue", "org.thingsboard.server.cache"}) public class ThingsboardSnmpTransportApplication { private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name"; diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 775a685ae8..89b38ead12 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -47,6 +47,7 @@ "flot": "git://github.com/thingsboard/flot.git#0.9-work", "flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master", "font-awesome": "^4.7.0", + "html2canvas": "^1.0.0-rc.7", "jquery": "^3.5.1", "jquery.terminal": "^2.18.3", "js-beautify": "^1.13.0", diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 37802a08ed..4d25e5ce72 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -294,10 +294,11 @@ export class AuthService { }) ); } else if (accessToken) { - this.utils.updateQueryParam('accessToken', null); + const queryParamsToRemove = ['accessToken']; if (refreshToken) { - this.utils.updateQueryParam('refreshToken', null); + queryParamsToRemove.push('refreshToken'); } + this.utils.removeQueryParams(queryParamsToRemove); try { this.updateAndValidateToken(accessToken, 'jwt_token', false); if (refreshToken) { diff --git a/ui-ngx/src/app/core/http/alarm.service.ts b/ui-ngx/src/app/core/http/alarm.service.ts index ab35c49b01..dc5771e31b 100644 --- a/ui-ngx/src/app/core/http/alarm.service.ts +++ b/ui-ngx/src/app/core/http/alarm.service.ts @@ -72,7 +72,7 @@ export class AlarmService { public getHighestAlarmSeverity(entityId: EntityId, alarmSearchStatus: AlarmSearchStatus, alarmStatus: AlarmStatus, config?: RequestConfig): Observable { - let url = `/api/alarm/highestSeverity/${entityId.entityType}/${entityId.entityType}`; + let url = `/api/alarm/highestSeverity/${entityId.entityType}/${entityId.id}`; if (alarmSearchStatus) { url += `?searchStatus=${alarmSearchStatus}`; } else if (alarmStatus) { diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 3c1c659e95..f8a4ad279b 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -410,9 +410,23 @@ export class UtilsService { return decodeURIComponent(results[2].replace(/\+/g, ' ')); } + public removeQueryParams(keys: Array) { + let params = this.window.location.search; + for (const key of keys) { + params = this.updateUrlQueryString(params, key, null); + } + const baseUrlPart = [baseUrl(), this.window.location.pathname].join(''); + this.window.history.replaceState({}, '', baseUrlPart + params); + } + public updateQueryParam(name: string, value: string | null) { const baseUrlPart = [baseUrl(), this.window.location.pathname].join(''); const urlQueryString = this.window.location.search; + const params = this.updateUrlQueryString(urlQueryString, name, value); + this.window.history.replaceState({}, '', baseUrlPart + params); + } + + private updateUrlQueryString(urlQueryString: string, name: string, value: string | null): string { let newParam = ''; let params = ''; if (value !== null) { @@ -425,13 +439,16 @@ export class UtilsService { newParam = '$1' + newParam; } params = urlQueryString.replace(keyRegex, newParam); + if (params.startsWith('&')) { + params = '?' + params.substring(1); + } } else if (newParam) { params = urlQueryString + '&' + newParam; } } else if (newParam) { params = '?' + newParam; } - this.window.history.replaceState({}, '', baseUrlPart + params); + return params; } public baseUrl(): string { diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts index 743b49c286..2bff69a53e 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts @@ -108,7 +108,7 @@ export class AlarmTableConfig extends EntityTableConfig } fetchAlarms(pageLink: TimePageLink): Observable> { - const query = new AlarmQuery(this.entityId, pageLink, this.searchStatus, null, true, null); + const query = new AlarmQuery(this.entityId, pageLink, this.searchStatus, null, true); return this.alarmService.getAlarms(query); } diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts index ad7b3c6a11..c4f0dedc4a 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts @@ -15,6 +15,7 @@ /// import { + ChangeDetectorRef, Component, Injector, Input, @@ -73,6 +74,7 @@ export class AliasesEntitySelectComponent implements OnInit, OnDestroy { constructor(private translate: TranslateService, private overlay: Overlay, + private cd: ChangeDetectorRef, private breakpointObserver: BreakpointObserver, private viewContainerRef: ViewContainerRef) { } @@ -179,6 +181,7 @@ export class AliasesEntitySelectComponent implements OnInit, OnDestroy { displayValue = this.translate.instant('entity.entities'); } this.displayValue = displayValue; + this.cd.detectChanges(); } private updateEntityAliasesInfo() { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.html new file mode 100644 index 0000000000..12d044f36b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.html @@ -0,0 +1,88 @@ + +
+ +

dashboard.update-image

+ + +
+ + +
+
+
+ + Top % + + +
+ + Left % + + +
+
{{ 'dashboard.no-image' | translate }}
+ +
+ + Right % + + +
+ + Bottom % + + +
+
+
+ +
+ + +
+
+
+ +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.scss new file mode 100644 index 0000000000..85f20ad3c9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.scss @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2021 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. + */ + +$previewSize: 300px !default; + +:host { + + mat-form-field.rect-field { + max-width: 60px; + } + + .tb-image-preview { + width: auto; + max-width: $previewSize - 2; + height: auto; + max-height: $previewSize - 2; + } + + .tb-image-preview-container { + position: relative; + float: left; + width: $previewSize; + height: $previewSize; + margin-right: 12px; + vertical-align: top; + border: solid 1px; + + div { + width: 100%; + font-size: 18px; + text-align: center; + } + + div, + .tb-image-preview { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + } + + .taking-screenshot-progress { + background-color: rgba(255, 255, 255, 0.65); + } +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.ts new file mode 100644 index 0000000000..0bc0206740 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.ts @@ -0,0 +1,174 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { DashboardId } from '@shared/models/id/dashboard-id'; +import { DashboardService } from '@core/http/dashboard.service'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import html2canvas from 'html2canvas'; +import { map, share } from 'rxjs/operators'; +import { BehaviorSubject, from } from 'rxjs'; +import { isNumber } from '@core/utils'; + +export interface DashboardImageDialogData { + dashboardId: DashboardId; + currentImage?: string; + dashboardElement: HTMLElement; +} + +export interface DashboardImageDialogResult { + image?: string; +} + +@Component({ + selector: 'tb-dashboard-image-dialog', + templateUrl: './dashboard-image-dialog.component.html', + styleUrls: ['./dashboard-image-dialog.component.scss'] +}) +export class DashboardImageDialogComponent extends DialogComponent { + + takingScreenshotSubject = new BehaviorSubject(false); + + takingScreenshot$ = this.takingScreenshotSubject.asObservable().pipe( + share() + ); + + dashboardId: DashboardId; + safeImageUrl?: SafeUrl; + dashboardElement: HTMLElement; + + dashboardRectFormGroup: FormGroup; + dashboardImageFormGroup: FormGroup; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DashboardImageDialogData, + public dialogRef: MatDialogRef, + private dashboardService: DashboardService, + private sanitizer: DomSanitizer, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.dashboardId = this.data.dashboardId; + this.updateImage(this.data.currentImage); + this.dashboardElement = this.data.dashboardElement; + const clientRect = this.dashboardElement.getBoundingClientRect(); + + this.dashboardRectFormGroup = this.fb.group({ + left: [0, [Validators.min(0), Validators.max(100)]], + top: [0, [Validators.min(0), Validators.max(100)]], + right: [100, [Validators.min(0), Validators.max(100)]], + bottom: [100, [Validators.min(0), Validators.max(100)]] + }); + + this.dashboardImageFormGroup = this.fb.group({ + dashboardImage: [this.data.currentImage] + }); + + this.dashboardImageFormGroup.get('dashboardImage').valueChanges.subscribe( + (newImage) => { + this.updateImage(newImage); + } + ); + } + + private convertUserPercent(percent: any, defaultValue: number): number { + let result: number; + if (isNumber(percent)) { + result = Math.max(0, Math.min(100, percent)) / 100; + } else { + result = defaultValue; + } + return result; + } + + takeScreenShot() { + this.takingScreenshotSubject.next(true); + const rect = this.dashboardElement.getBoundingClientRect(); + + const leftVal = this.convertUserPercent(this.dashboardRectFormGroup.get('left').value, 0); + const topVal = this.convertUserPercent(this.dashboardRectFormGroup.get('top').value, 0); + const rightVal = this.convertUserPercent(this.dashboardRectFormGroup.get('right').value, 100); + const bottomVal = this.convertUserPercent(this.dashboardRectFormGroup.get('bottom').value, 100); + + const left = leftVal * rect.width; + const top = topVal * rect.height; + const right = rightVal * rect.width; + const bottom = bottomVal * rect.height; + + const x = rect.left + left; + const y = rect.top + top; + let width = right - left; + let height = bottom - top; + width = Math.max(1, width); + height = Math.max(1, height); + from(html2canvas(this.dashboardElement, { + logging: false, + useCORS: true, + foreignObjectRendering: false, + scale: 512 / width, + x, + y, + width, + height + })).pipe( + map(canvas => canvas.toDataURL())).subscribe( + (image) => { + this.updateImage(image); + this.dashboardImageFormGroup.patchValue({dashboardImage: image}, {emitEvent: false}); + this.dashboardImageFormGroup.markAsDirty(); + this.takingScreenshotSubject.next(false); + }, + (e) => { + this.takingScreenshotSubject.next(false); + } + ); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dashboardService.getDashboard(this.dashboardId.id).subscribe( + (dashboard) => { + const newImage: string = this.dashboardImageFormGroup.get('dashboardImage').value; + dashboard.image = newImage; + this.dashboardService.saveDashboard(dashboard).subscribe( + () => { + this.dialogRef.close({ + image: newImage + }); + } + ); + } + ); + } + + private updateImage(imageUrl: string) { + if (imageUrl) { + this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(imageUrl); + } else { + this.safeImageUrl = null; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index 23555da05d..5c54fe23e0 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -81,6 +81,12 @@ (click)="isFullscreen = !isFullscreen"> {{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }} + - - - +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts index 60fb27a752..0a943ab5af 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts @@ -23,6 +23,7 @@ import { BINDING_MODE, BINDING_MODE_NAMES, getDefaultProfileConfig, + Instance, INSTANCES, KEY_NAME, Lwm2mProfileConfigModels, @@ -37,6 +38,7 @@ import { DeviceProfileService } from '@core/http/device-profile.service'; import { deepClone, isDefinedAndNotNull, isEmpty, isUndefined } from '@core/utils'; import { JsonArray, JsonObject } from '@angular/compiler-cli/ngcc/src/packages/entry_point'; import { Direction } from '@shared/models/page/sort-order'; +import _ from 'lodash'; @Component({ selector: 'tb-profile-lwm2m-device-transport-configuration', @@ -230,24 +232,30 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro const isNotZeroInstanceId = (instance) => !instance.includes('/0/'); return attribute.some(isNotZeroInstanceId) || telemetry.some(isNotZeroInstanceId); } - private addInstances = (attribute: string[], telemetry: string[], clientObserveAttrTelemetry: ObjectLwM2M[]): void => { const instancesPath = attribute.concat(telemetry) .filter(instance => !instance.includes('/0/')) .map(instance => instance.slice(1, instance.lastIndexOf('/'))) .sort(this.sortPath); - new Set(instancesPath).forEach(path => { const pathParameter = Array.from(path.split('/'), String); const objectLwM2M = clientObserveAttrTelemetry.find(x => x.keyId === pathParameter[0]); if (objectLwM2M) { - const instance = deepClone(objectLwM2M.instances[0]); - instance.id = +pathParameter[1]; + const instance = this.updateInInstanceKeyName (objectLwM2M.instances[0], +pathParameter[1]); objectLwM2M.instances.push(instance); } }); } + private updateInInstanceKeyName = (instance: Instance, instanceId: number): Instance => { + const instanceUpdate = deepClone(instance); + instanceUpdate.id = instanceId; + instanceUpdate.resources.forEach(resource => { + resource.keyName = _.camelCase(resource.name + instanceUpdate.id); + }) + return instanceUpdate; + } + private updateObserveAttrTelemetryObjects = (parameters: string[], objectLwM2MS: ObjectLwM2M[], nameParameter: string): void => { parameters.forEach(parameter => { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts index 3d6b93b84c..530679113c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts @@ -45,6 +45,7 @@ import { Lwm2mObjectAddInstancesData, Lwm2mObjectAddInstancesDialogComponent } from '@home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component'; +import _ from 'lodash'; @Component({ selector: 'tb-profile-lwm2m-observe-attr-telemetry', @@ -261,7 +262,6 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor r.attribute = false; r.telemetry = false; r.observe = false; - r.keyName = {}; r.attributeLwm2m = {}; }); const valueOld = this.instancesToSetId(instancesArray); @@ -280,7 +280,8 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor private addInstancesNew = (idsAdd: Set, objectLwM2MFormGroup: FormGroup, instancesFormArray: FormArray, instanceNew: Instance): void => { idsAdd.forEach(x => { - this.pushInstance(instancesFormArray, x, instanceNew); + instanceNew.resources.forEach(resource => {resource.keyName = _.camelCase(resource.name + x);}); + this.pushInstance(instancesFormArray, x, deepClone(instanceNew as Instance)); }); (instancesFormArray.controls as FormGroup[]).sort((a, b) => a.value.id - b.value.id); } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts index f6e58df903..acea41394a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts @@ -246,7 +246,7 @@ export interface ResourceLwM2M { observe: boolean; attribute: boolean; telemetry: boolean; - keyName: {}; + keyName: string; attributeLwm2m?: {}; } diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index da7598488e..13e3976299 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -196,6 +196,18 @@ {{ 'tenant-profile.max-sms-range' | translate}} + + tenant-profile.max-created-alarms + + + {{ 'tenant-profile.max-created-alarms-required' | translate}} + + + {{ 'tenant-profile.max-created-alarms-range' | translate}} + + tenant-profile.transport-tenant-msg-rate-limit diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts index e78ad83ebd..ad00df7476 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts @@ -73,6 +73,7 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA maxRuleNodeExecutionsPerMessage: [null, [Validators.required, Validators.min(0)]], maxEmails: [null, [Validators.required, Validators.min(0)]], maxSms: [null, [Validators.required, Validators.min(0)]], + maxCreatedAlarms: [null, [Validators.required, Validators.min(0)]], defaultStorageTtlDays: [null, [Validators.required, Validators.min(0)]] }); this.defaultTenantProfileConfigurationFormGroup.valueChanges.subscribe(() => { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html new file mode 100644 index 0000000000..09f3199b06 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html @@ -0,0 +1,112 @@ + +
+
+
+ + {{widget.titleIcon}} + {{widget.customTranslatedTitle}} + + + +
+
+ + + + + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss new file mode 100644 index 0000000000..ed66a2b235 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss @@ -0,0 +1,117 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + +} + +tb-widget.tb-widget { + position: relative; + height: 100%; + margin: 0; + overflow: hidden; + outline: none; + + transition: all .2s ease-in-out; +} + +div.tb-widget { + position: relative; + height: 100%; + margin: 0; + overflow: hidden; + outline: none; + + transition: all .2s ease-in-out; + + .tb-widget-title { + max-height: 65px; + padding-top: 5px; + padding-left: 5px; + overflow: hidden; + + tb-timewindow { + font-size: 14px; + opacity: .85; + margin: 0; + } + + .title { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + line-height: 24px; + letter-spacing: .01em; + margin: 0; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + + &.single-row{ + -webkit-line-clamp: 1; + } + } + } + + .tb-widget-actions { + z-index: 19; + margin: 5px 0 0; + + &-absolute { + position: absolute; + top: 3px; + right: 8px; + z-index: 150; + } + + button.mat-icon-button { + width: 32px; + min-width: 32px; + height: 32px; + min-height: 32px; + padding: 0 !important; + margin: 0 !important; + line-height: 20px; + + mat-icon { + width: 20px; + min-width: 20px; + height: 20px; + min-height: 20px; + font-size: 20px; + line-height: 20px; + } + } + } + + .tb-widget-content { + &.tb-no-interaction { + pointer-events: none; + } + tb-widget { + position: relative; + width: 100%; + } + } + + &.tb-highlighted { + border: 1px solid #039be5; + box-shadow: 0 0 20px #039be5; + } + + &.tb-not-highlighted { + opacity: .5; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts new file mode 100644 index 0000000000..cd2b322420 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts @@ -0,0 +1,153 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnInit, + Output +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { DashboardWidget, DashboardWidgets } from '@home/models/dashboard-component.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { SafeStyle } from '@angular/platform-browser'; + +export enum WidgetComponentActionType { + MOUSE_DOWN, + CLICKED, + CONTEXT_MENU, + EDIT, + EXPORT, + REMOVE +} + +export class WidgetComponentAction { + event: MouseEvent; + actionType: WidgetComponentActionType; +} + +@Component({ + selector: 'tb-widget-container', + templateUrl: './widget-container.component.html', + styleUrls: ['./widget-container.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class WidgetContainerComponent extends PageComponent implements OnInit { + + @Input() + widget: DashboardWidget; + + @Input() + dashboardStyle: {[klass: string]: any}; + + @Input() + backgroundImage: SafeStyle | string; + + @Input() + isEdit: boolean; + + @Input() + isMobile: boolean; + + @Input() + dashboardWidgets: DashboardWidgets; + + @Input() + isEditActionEnabled: boolean; + + @Input() + isExportActionEnabled: boolean; + + @Input() + isRemoveActionEnabled: boolean; + + @Input() + disableWidgetInteraction = false; + + @Output() + widgetFullscreenChanged: EventEmitter = new EventEmitter(); + + @Output() + widgetComponentAction: EventEmitter = new EventEmitter(); + + constructor(protected store: Store, + private cd: ChangeDetectorRef) { + super(store); + } + + ngOnInit(): void { + this.widget.widgetContext.containerChangeDetector = this.cd; + } + + isHighlighted(widget: DashboardWidget) { + return this.dashboardWidgets.isHighlighted(widget); + } + + isNotHighlighted(widget: DashboardWidget) { + return this.dashboardWidgets.isNotHighlighted(widget); + } + + onFullscreenChanged(expanded: boolean) { + this.widgetFullscreenChanged.emit(expanded); + } + + onMouseDown(event: MouseEvent) { + this.widgetComponentAction.emit({ + event, + actionType: WidgetComponentActionType.MOUSE_DOWN + }); + } + + onClicked(event: MouseEvent) { + this.widgetComponentAction.emit({ + event, + actionType: WidgetComponentActionType.CLICKED + }); + } + + onContextMenu(event: MouseEvent) { + this.widgetComponentAction.emit({ + event, + actionType: WidgetComponentActionType.CONTEXT_MENU + }); + } + + onEdit(event: MouseEvent) { + this.widgetComponentAction.emit({ + event, + actionType: WidgetComponentActionType.EDIT + }); + } + + onExport(event: MouseEvent) { + this.widgetComponentAction.emit({ + event, + actionType: WidgetComponentActionType.EXPORT + }); + } + + onRemove(event: MouseEvent) { + this.widgetComponentAction.emit({ + event, + actionType: WidgetComponentActionType.REMOVE + }); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 3044f29bca..fea6e14291 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -468,10 +468,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI ); } - private detectChanges() { + private detectChanges(detectContainerChanges = false) { if (!this.destroyed) { try { this.cd.detectChanges(); + if (detectContainerChanges) { + this.widgetContext.detectContainerChanges(); + } } catch (e) { // console.log(e); } @@ -491,6 +494,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } if (!this.widgetContext.inited && this.isReady()) { this.widgetContext.inited = true; + this.widgetContext.detectContainerChanges(); if (this.cafs.init) { this.cafs.init(); this.cafs.init = null; @@ -878,7 +882,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI timeWindowUpdated: (subscription, timeWindowConfig) => { this.ngZone.run(() => { this.widget.config.timewindow = timeWindowConfig; - this.detectChanges(); + this.detectChanges(true); }); } }; diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts index a0000e40be..58724b89f9 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts @@ -14,13 +14,14 @@ /// limitations under the License. /// -import { Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { MenuSection } from '@core/services/menu.models'; @Component({ selector: 'tb-menu-link', templateUrl: './menu-link.component.html', - styleUrls: ['./menu-link.component.scss'] + styleUrls: ['./menu-link.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class MenuLinkComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts index a3afd99508..6bbc5414fe 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts @@ -14,14 +14,15 @@ /// limitations under the License. /// -import { Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { MenuSection } from '@core/services/menu.models'; import { Router } from '@angular/router'; @Component({ selector: 'tb-menu-toggle', templateUrl: './menu-toggle.component.html', - styleUrls: ['./menu-toggle.component.scss'] + styleUrls: ['./menu-toggle.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class MenuToggleComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.ts b/ui-ngx/src/app/modules/home/menu/side-menu.component.ts index aa96b517f9..c2b974cd6b 100644 --- a/ui-ngx/src/app/modules/home/menu/side-menu.component.ts +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.ts @@ -14,14 +14,15 @@ /// limitations under the License. /// -import { Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { MenuService } from '@core/services/menu.service'; import { MenuSection } from '@core/services/menu.models'; @Component({ selector: 'tb-side-menu', templateUrl: './side-menu.component.html', - styleUrls: ['./side-menu.component.scss'] + styleUrls: ['./side-menu.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class SideMenuComponent implements OnInit { diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts index b1ded98ba7..2edd7f9afa 100644 --- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts @@ -286,8 +286,8 @@ export class DashboardWidgets implements Iterable { export class DashboardWidget implements GridsterItem, IDashboardWidget { - highlighted = false; - selected = false; + private highlightedValue = false; + private selectedValue = false; isFullscreen = false; @@ -334,6 +334,28 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { this.gridsterItemComponentSubject.complete(); } + get highlighted() { + return this.highlightedValue; + } + + set highlighted(highlighted: boolean) { + if (this.highlightedValue !== highlighted) { + this.highlightedValue = highlighted; + this.widgetContext.detectContainerChanges(); + } + } + + get selected() { + return this.selectedValue; + } + + set selected(selected: boolean) { + if (this.selectedValue !== selected) { + this.selectedValue = selected; + this.widgetContext.detectContainerChanges(); + } + } + constructor( private dashboard: IDashboardComponent, public widget: Widget, @@ -342,7 +364,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { widget.id = guid(); } this.widgetId = widget.id; - this.updateWidgetParams(); + this.updateWidgetParams(false); } gridsterItemComponent$(): Observable { @@ -353,7 +375,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { } } - updateWidgetParams() { + updateWidgetParams(detectChanges = true) { this.color = this.widget.config.color || 'rgba(0, 0, 0, 0.87)'; this.backgroundColor = this.widget.config.backgroundColor || '#fff'; this.padding = this.widget.config.padding || '8px'; @@ -405,6 +427,9 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { this.customHeaderActions = this.widgetContext.customHeaderActions ? this.widgetContext.customHeaderActions : []; this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : []; + if (detectChanges) { + this.widgetContext.detectContainerChanges(); + } } @enumerable(true) diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 64fbe59efa..50466d7600 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -136,6 +136,10 @@ export class WidgetContext { this.changeDetectorValue = cd; } + set containerChangeDetector(cd: ChangeDetectorRef) { + this.containerChangeDetectorValue = cd; + } + get currentUser(): AuthUser { if (this.store) { return getCurrentAuthUser(this.store); @@ -162,6 +166,7 @@ export class WidgetContext { router: Router; private changeDetectorValue: ChangeDetectorRef; + private containerChangeDetectorValue: ChangeDetectorRef; inited = false; destroyed = false; @@ -309,6 +314,16 @@ export class WidgetContext { } } + detectContainerChanges() { + if (!this.destroyed) { + try { + this.containerChangeDetectorValue.detectChanges(); + } catch (e) { + // console.log(e); + } + } + } + updateWidgetParams() { if (!this.destroyed) { setTimeout(() => { diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html index 1fb8978f22..ca7512d7ec 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html @@ -105,6 +105,11 @@ {{ 'dashboard.title-required' | translate }}
+ +
dashboard.description diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts index 37030c57f1..70914e069d 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts @@ -80,6 +80,7 @@ export class DashboardFormComponent extends EntityComponent { return this.fb.group( { title: [entity ? entity.title : '', [Validators.required]], + image: [entity ? entity.image : null], configuration: this.fb.group( { description: [entity && entity.configuration ? entity.configuration.description : ''], @@ -92,6 +93,7 @@ export class DashboardFormComponent extends EntityComponent { updateForm(entity: Dashboard) { this.updateFields(entity); this.entityForm.patchValue({title: entity.title}); + this.entityForm.patchValue({image: entity.image}); this.entityForm.patchValue({configuration: {description: entity.configuration ? entity.configuration.description : ''}}); } diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts index 730ac2603a..3225c73b2c 100644 --- a/ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts @@ -104,7 +104,8 @@ export class EntityViewsTableConfigResolver implements Resolve this.onEntityViewAction(action); - this.config.detailsReadonly = () => (this.config.componentsData.entityViewScope === 'customer_user' || this.config.componentsData.entityViewScope === 'edge_customer_user'); + this.config.detailsReadonly = () => (this.config.componentsData.entityViewScope === 'customer_user' || + this.config.componentsData.entityViewScope === 'edge_customer_user'); this.config.headerComponent = EntityViewTableHeaderComponent; @@ -151,7 +152,8 @@ export class EntityViewsTableConfigResolver implements Resolve this.config.componentsData.entityViewScope === 'tenant'; return this.config; @@ -243,7 +245,7 @@ export class EntityViewsTableConfigResolver implements Resolve true, + isEnabled: () => true, onAction: ($event, entity) => this.unassignFromEdge($event, entity) } ); diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts index 7f485df45c..79accbe99b 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { MenuService } from '@core/services/menu.service'; import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; @@ -25,7 +25,8 @@ import { HomeDashboard } from '@shared/models/dashboard.models'; @Component({ selector: 'tb-home-links', templateUrl: './home-links.component.html', - styleUrls: ['./home-links.component.scss'] + styleUrls: ['./home-links.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class HomeLinksComponent implements OnInit { @@ -37,6 +38,7 @@ export class HomeLinksComponent implements OnInit { constructor(private menuService: MenuService, public breakpointObserver: BreakpointObserver, + private cd: ChangeDetectorRef, private route: ActivatedRoute) { } @@ -57,6 +59,7 @@ export class HomeLinksComponent implements OnInit { if (this.breakpointObserver.isMatched(MediaBreakpoints['gt-lg'])) { this.cols = 4; } + this.cd.detectChanges(); } sectionColspan(section: HomeSection): number { diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.ts b/ui-ngx/src/app/shared/components/breadcrumb.component.ts index c967c3815e..3fd2f05ff3 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { BehaviorSubject, Subject } from 'rxjs'; import { BreadCrumb, BreadCrumbConfig } from './breadcrumb'; import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; @@ -25,7 +25,8 @@ import { guid } from '@core/utils'; @Component({ selector: 'tb-breadcrumb', templateUrl: './breadcrumb.component.html', - styleUrls: ['./breadcrumb.component.scss'] + styleUrls: ['./breadcrumb.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class BreadcrumbComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts index 9a754fa81d..782f0787dd 100644 --- a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts @@ -139,6 +139,11 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; + if (this.disabled) { + this.selectDashboardFormGroup.disable({emitEvent: false}); + } else { + this.selectDashboardFormGroup.enable({emitEvent: false}); + } } writeValue(value: DashboardInfo | string | null): void { diff --git a/ui-ngx/src/app/shared/components/image-input.component.html b/ui-ngx/src/app/shared/components/image-input.component.html index 4b7e4fe1d4..2fe8cc4102 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.html +++ b/ui-ngx/src/app/shared/components/image-input.component.html @@ -21,10 +21,10 @@ [flowConfig]="{singleFile: true, allowDuplicateUploads: true}">
-
dashboard.no-image
+
{{ (disabled ? 'dashboard.empty-image' : 'dashboard.no-image') | translate }}
-
+
-
@@ -42,5 +42,5 @@
-
dashboard.maximum-upload-file-size
+
dashboard.maximum-upload-file-size
diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.ts b/ui-ngx/src/app/shared/components/time/timewindow.component.ts index 0169feaae7..77d10eadcc 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.ts @@ -15,6 +15,7 @@ /// import { + ChangeDetectorRef, Component, forwardRef, Inject, @@ -158,6 +159,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe, private datePipe: DatePipe, private overlay: Overlay, + private cd: ChangeDetectorRef, public viewContainerRef: ViewContainerRef, public breakpointObserver: BreakpointObserver, @Inject(DOCUMENT) private document: Document, @@ -313,6 +315,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces } else { this.innerValue.displayTimezoneAbbr = ''; } + this.cd.detectChanges(); } hideLabel() { diff --git a/ui-ngx/src/app/shared/components/user-menu.component.ts b/ui-ngx/src/app/shared/components/user-menu.component.ts index 8598eed5d8..f27f272195 100644 --- a/ui-ngx/src/app/shared/components/user-menu.component.ts +++ b/ui-ngx/src/app/shared/components/user-menu.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { User } from '@shared/models/user.model'; import { Authority } from '@shared/models/authority.enum'; import { select, Store } from '@ngrx/store'; @@ -27,7 +27,8 @@ import { Router } from '@angular/router'; @Component({ selector: 'tb-user-menu', templateUrl: './user-menu.component.html', - styleUrls: ['./user-menu.component.scss'] + styleUrls: ['./user-menu.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class UserMenuComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index a76bf1b425..e629e1c574 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -21,7 +21,7 @@ import { EntityId } from '@shared/models/id/entity-id'; import { TimePageLink } from '@shared/models/page/page-link'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { EntityType } from '@shared/models/entity-type.models'; -import { isString } from '@core/utils'; +import { CustomerId } from '@shared/models/id/customer-id'; export enum AlarmSeverity { CRITICAL = 'CRITICAL', @@ -87,6 +87,7 @@ export const alarmSeverityColors = new Map( export interface Alarm extends BaseData { tenantId: TenantId; + customerId: CustomerId; type: string; originator: EntityId; severity: AlarmSeverity; @@ -110,6 +111,7 @@ export interface AlarmDataInfo extends AlarmInfo { export const simulatedAlarm: AlarmInfo = { id: new AlarmId(NULL_UUID), tenantId: new TenantId(NULL_UUID), + customerId: new CustomerId(NULL_UUID), createdTime: new Date().getTime(), startTs: new Date().getTime(), endTs: 0, @@ -201,17 +203,15 @@ export class AlarmQuery { searchStatus: AlarmSearchStatus; status: AlarmStatus; fetchOriginator: boolean; - offset: string; constructor(entityId: EntityId, pageLink: TimePageLink, searchStatus: AlarmSearchStatus, status: AlarmStatus, - fetchOriginator: boolean, offset: string) { + fetchOriginator: boolean) { this.affectedEntityId = entityId; this.pageLink = pageLink; this.searchStatus = searchStatus; this.status = status; this.fetchOriginator = fetchOriginator; - this.offset = offset; } public toQuery(): string { @@ -225,9 +225,6 @@ export class AlarmQuery { if (typeof this.fetchOriginator !== 'undefined' && this.fetchOriginator !== null) { query += `&fetchOriginator=${this.fetchOriginator}`; } - if (isString(this.offset) && this.offset.length) { - query += `&offset=${this.offset}`; - } return query; } diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index 6a90101923..a782fe8256 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -26,6 +26,7 @@ import { Filters } from '@shared/models/query/query.models'; export interface DashboardInfo extends BaseData { tenantId?: TenantId; title?: string; + image?: string; assignedCustomers?: Array; } @@ -90,6 +91,7 @@ export interface DashboardSettings { dashboardLogoUrl?: string; showDashboardTimewindow?: boolean; showDashboardExport?: boolean; + showUpdateDashboardImage?: boolean; toolbarAlwaysOpen?: boolean; titleColor?: string; } diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 683d9ddcc1..fe7868d584 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -28,6 +28,7 @@ import { TimeUnit } from '@shared/models/time/time.models'; import * as _moment from 'moment'; import { AbstractControl, ValidationErrors } from '@angular/forms'; import { FirmwareId } from '@shared/models/id/firmware-id'; +import { DashboardId } from '@shared/models/id/dashboard-id'; export enum DeviceProfileType { DEFAULT = 'DEFAULT', @@ -492,10 +493,12 @@ export interface DeviceProfile extends BaseData { description?: string; default?: boolean; type: DeviceProfileType; + image?: string; transportType: DeviceTransportType; provisionType: DeviceProvisionType; provisionDeviceKey?: string; defaultRuleChainId?: RuleChainId; + defaultDashboardId?: DashboardId; defaultQueueName?: string; firmwareId?: FirmwareId; softwareId?: FirmwareId; @@ -505,6 +508,8 @@ export interface DeviceProfile extends BaseData { export interface DeviceProfileInfo extends EntityInfoData { type: DeviceProfileType; transportType: DeviceTransportType; + image?: string; + defaultDashboardId?: DashboardId; } export interface DefaultDeviceConfiguration { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d8a21107bc..93c5c89955 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -666,6 +666,9 @@ "no-widgets": "No widgets configured", "add-widget": "Add new widget", "title": "Title", + "image": "Dashboard image", + "update-image": "Update dashboard image", + "take-screenshot": "Take screenshot", "select-widget-title": "Select widget", "select-widget-value": "{{title}}: select widget", "select-widget-subtitle": "List of available widget types", @@ -712,6 +715,7 @@ "background-image": "Background image", "background-size-mode": "Background size mode", "no-image": "No image selected", + "empty-image": "No image", "drop-image": "Drop an image or click to select a file to upload.", "maximum-upload-file-size": "Maximum upload file size: {{ size }}", "cannot-upload-file": "Cannot upload file", @@ -746,6 +750,7 @@ "display-filters": "Display filters", "display-dashboard-timewindow": "Display timewindow", "display-dashboard-export": "Display export", + "display-update-dashboard-image": "Display update dashboard image", "display-dashboard-logo": "Display logo in dashboard fullscreen mode", "dashboard-logo-image": "Dashboard logo image", "import": "Import dashboard", @@ -1022,6 +1027,7 @@ "type": "Profile type", "type-required": "Profile type is required.", "type-default": "Default", + "image": "Device profile image", "transport-type": "Transport type", "transport-type-required": "Transport type is required.", "transport-type-default": "Default", @@ -1039,6 +1045,7 @@ "profile-configuration": "Profile configuration", "transport-configuration": "Transport configuration", "default-rule-chain": "Default rule chain", + "default-dashboard": "Default dashboard", "select-queue-hint": "Select from a drop-down list or add a custom name.", "delete-device-profile-title": "Are you sure you want to delete the device profile '{{deviceProfileName}}'?", "delete-device-profile-text": "Be careful, after the confirmation the device profile and all related data will become unrecoverable.", @@ -1221,7 +1228,6 @@ "required": " value is required.", "mode": "Security config mode", "pattern_hex_dec": "{ count, plural, 0 {must be hex decimal format} other {must be # characters} }", - "bootstrap-tab": "Bootstrap", "servers": "Servers", "short-id": "Short ID", "short-id-tip": "Short Server ID", @@ -2518,7 +2524,10 @@ "max-emails-range": "Maximum number of emails sent can't be negative", "max-sms": "Maximum number of SMS sent (0 - unlimited)", "max-sms-required": "Maximum number of SMS sent is required.", - "max-sms-range": "Maximum number of SMS sent can't be negative" + "max-sms-range": "Maximum number of SMS sent can't be negative", + "max-created-alarms": "Maximum number of alarms created (0 - unlimited)", + "max-created-alarms-required": "Maximum number of alarms created is required.", + "max-created-alarms-range": "Maximum number of alarms created can't be negative" }, "timeinterval": { "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index b1c5167ac9..d7ed0d4e41 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -2222,6 +2222,11 @@ base64-arraybuffer@0.1.5: resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= +base64-arraybuffer@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45" + integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ== + base64-js@^1.0.2: version "1.3.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" @@ -3206,6 +3211,13 @@ css-declaration-sorter@^4.0.1: postcss "^7.0.1" timsort "^0.3.0" +css-line-break@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-1.1.1.tgz#d5e9bdd297840099eb0503c7310fd34927a026ef" + integrity sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA== + dependencies: + base64-arraybuffer "^0.2.0" + css-loader@4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.2.2.tgz#b668b3488d566dc22ebcf9425c5f254a05808c89" @@ -4782,6 +4794,13 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +html2canvas@^1.0.0-rc.7: + version "1.0.0-rc.7" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.0.0-rc.7.tgz#70c159ce0e63954a91169531894d08ad5627ac98" + integrity sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA== + dependencies: + css-line-break "1.1.1" + http-cache-semantics@^3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"