diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 2dc246d9a7..bd09b04a02 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.entitiy.alarm; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -29,20 +30,20 @@ import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; -import org.thingsboard.server.common.data.alarm.AlarmQueryV2; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; @Service @AllArgsConstructor @@ -104,19 +105,8 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } AlarmInfo alarmInfo = result.getAlarm(); if (result.isModified()) { - AlarmComment alarmComment = AlarmComment.builder() - .alarmId(alarm.getId()) - .type(AlarmCommentType.SYSTEM) - .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was acknowledged by user %s", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) - .put("userId", user.getId().toString()) - .put("subtype", "ACK")) - .build(); - try { - alarmCommentService.saveAlarmComment(alarm, alarmComment, user); - } catch (ThingsboardException e) { - log.error("Failed to save alarm comment", e); - } + String systemComment = String.format("Alarm was acknowledged by user %s", user.getTitle()); + addSystemAlarmComment(alarmInfo, user, "ACK", systemComment); notificationEntityService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, alarmInfo.getCustomerId(), ActionType.ALARM_ACK, user); } else { @@ -138,19 +128,8 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } AlarmInfo alarmInfo = result.getAlarm(); if (result.isCleared()) { - AlarmComment alarmComment = AlarmComment.builder() - .alarmId(alarm.getId()) - .type(AlarmCommentType.SYSTEM) - .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was cleared by user %s", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) - .put("userId", user.getId().toString()) - .put("subtype", "CLEAR")) - .build(); - try { - alarmCommentService.saveAlarmComment(alarm, alarmComment, user); - } catch (ThingsboardException e) { - log.error("Failed to save alarm comment", e); - } + String systemComment = String.format("Alarm was cleared by user %s", user.getTitle()); + addSystemAlarmComment(alarmInfo, user, "CLEAR", systemComment); notificationEntityService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, alarmInfo.getCustomerId(), ActionType.ALARM_CLEAR, user); } else { @@ -168,21 +147,8 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb AlarmInfo alarmInfo = result.getAlarm(); if (result.isModified()) { AlarmAssignee assignee = alarmInfo.getAssignee(); - AlarmComment alarmComment = AlarmComment.builder() - .alarmId(alarm.getId()) - .type(AlarmCommentType.SYSTEM) - .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was assigned by user %s to user %s", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName(), - (assignee.getFirstName() == null || assignee.getLastName() == null) ? assignee.getEmail() : assignee.getFirstName() + " " + assignee.getLastName())) - .put("userId", user.getId().toString()) - .put("assigneeId", assignee.getId().toString()) - .put("subtype", "ASSIGN")) - .build(); - try { - alarmCommentService.saveAlarmComment(alarm, alarmComment, user); - } catch (ThingsboardException e) { - log.error("Failed to save alarm comment", e); - } + String systemComment = String.format("Alarm was assigned by user %s to user %s", user.getTitle(), assignee.getTitle()); + addSystemAlarmComment(alarmInfo, user, "ASSIGN", systemComment, assignee.getId()); notificationEntityService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, alarmInfo.getCustomerId(), ActionType.ALARM_ASSIGNED, user); } else { @@ -199,19 +165,8 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } AlarmInfo alarmInfo = result.getAlarm(); if (result.isModified()) { - AlarmComment alarmComment = AlarmComment.builder() - .alarmId(alarm.getId()) - .type(AlarmCommentType.SYSTEM) - .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was unassigned by user %s", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) - .put("userId", user.getId().toString()) - .put("subtype", "ASSIGN")) - .build(); - try { - alarmCommentService.saveAlarmComment(alarm, alarmComment, user); - } catch (ThingsboardException e) { - log.error("Failed to save alarm comment", e); - } + String systemComment = String.format("Alarm was unassigned by user %s", user.getTitle()); + addSystemAlarmComment(alarmInfo, user, "ASSIGN", systemComment); notificationEntityService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarmInfo, alarmInfo.getCustomerId(), ActionType.ALARM_UNASSIGNED, user); } else { @@ -221,37 +176,20 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } @Override - public void unassignUserAlarms(TenantId tenantId, User user, long unassignTs) { - AlarmQueryV2 alarmQuery = AlarmQueryV2.builder().assigneeId(user.getId()).pageLink(new TimePageLink(Integer.MAX_VALUE)).build(); - try { - List alarms = alarmService.findAlarmsV2(tenantId, alarmQuery).get(30, TimeUnit.SECONDS).getData(); - for (AlarmInfo alarm : alarms) { - AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(tenantId, alarm.getId(), getOrDefault(unassignTs)); - if (!result.isSuccessful()) { - continue; - } - if (result.isModified()) { - AlarmComment alarmComment = AlarmComment.builder() - .alarmId(alarm.getId()) - .type(AlarmCommentType.SYSTEM) - .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was unassigned because user %s - was deleted", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) - .put("userId", user.getId().toString()) - .put("subtype", "ASSIGN")) - .build(); - try { - alarmCommentService.saveAlarmComment(alarm, alarmComment, user); - } catch (ThingsboardException e) { - log.error("Failed to save alarm comment", e); - } - notificationEntityService.logEntityAction(alarm.getTenantId(), alarm.getOriginator(), result.getAlarm(), - alarm.getCustomerId(), ActionType.ALARM_UNASSIGNED, user); - } + public List unassignDeletedUserAlarms(TenantId tenantId, User user, long unassignTs) { + List totalAlarmIds = new ArrayList<>(); + PageLink pageLink = new PageLink(100, 0, null, new SortOrder("id", SortOrder.Direction.ASC)); + while (true) { + PageData pageData = alarmService.findAlarmIdsByAssigneeId(user.getTenantId(), user.getId(), pageLink); + List alarmIds = pageData.getData(); + if (alarmIds.isEmpty()) { + break; } - - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new RuntimeException(e); + processAlarmsUnassignment(tenantId, user, alarmIds, unassignTs); + totalAlarmIds.addAll(alarmIds); + pageLink = pageLink.nextPageLink(); } + return totalAlarmIds; } @Override @@ -265,4 +203,46 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb private static long getOrDefault(long ts) { return ts > 0 ? ts : System.currentTimeMillis(); } + + private void processAlarmsUnassignment(TenantId tenantId, User user, List alarmIds, long unassignTs) { + for (AlarmId alarmId : alarmIds) { + log.trace("[{}] Unassigning alarm {} userId {}", tenantId, alarmId, user.getId()); + AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(user.getTenantId(), alarmId, unassignTs); + if (!result.isSuccessful()) { + log.error("[{}] Cannot unassign alarm {} userId {}", tenantId, alarmId, user.getId()); + continue; + } + if (result.isModified()) { + String comment = String.format("Alarm was unassigned because user %s - was deleted", user.getTitle()); + addSystemAlarmComment(result.getAlarm(), null, "ASSIGN", comment); + notificationEntityService.logEntityAction(result.getAlarm().getTenantId(), result.getAlarm().getOriginator(), result.getAlarm(), result.getAlarm().getCustomerId(), ActionType.ALARM_UNASSIGNED, null); + } + } + } + + private void addSystemAlarmComment(Alarm alarm, User user, String subType, String commentText) { + addSystemAlarmComment(alarm, user, subType, commentText, null); + } + + private void addSystemAlarmComment(Alarm alarm, User user, String subType, String commentText, UserId assigneeId) { + ObjectNode commentNode = JacksonUtil.newObjectNode(); + commentNode.put("text", commentText) + .put("subtype", subType); + if (user != null) { + commentNode.put("userId", user.getId().getId().toString()); + } + if (assigneeId != null) { + commentNode.put("assigneeId", assigneeId.getId().toString()); + } + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(commentNode) + .build(); + try { + alarmCommentService.saveAlarmComment(alarm, alarmComment, user); + } catch (ThingsboardException e) { + log.error("Failed to save alarm comment", e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java index 24af185539..24af55f38b 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java @@ -19,9 +19,12 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import java.util.List; + public interface TbAlarmService { Alarm save(Alarm entity, User user) throws ThingsboardException; @@ -38,7 +41,7 @@ public interface TbAlarmService { AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException; - void unassignUserAlarms(TenantId tenantId, User user, long unassignTs); + List unassignDeletedUserAlarms(TenantId tenantId, User user, long unassignTs); Boolean delete(Alarm alarm, User user); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java index 58bd40d070..2c2db7c6c7 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java @@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; -import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.servlet.http.HttpServletRequest; @@ -44,7 +43,6 @@ import static org.thingsboard.server.controller.UserController.ACTIVATE_URL_PATT public class DefaultUserService extends AbstractTbEntityService implements TbUserService { private final UserService userService; - private final TbAlarmService tbAlarmService; private final MailService mailService; private final SystemSecurityService systemSecurityService; @@ -64,7 +62,7 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse try { mailService.sendActivationEmail(activateUrl, email); } catch (ThingsboardException e) { - userService.deleteUser(tenantId, savedUser.getId()); + userService.deleteUser(tenantId, savedUser); throw e; } } @@ -77,17 +75,16 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse } @Override - public void delete(TenantId tenantId, CustomerId customerId, User tbUser, User user) throws ThingsboardException { + public void delete(TenantId tenantId, CustomerId customerId, User user, User responsibleUser) throws ThingsboardException { ActionType actionType = ActionType.DELETED; - UserId userId = tbUser.getId(); + UserId userId = user.getId(); try { - tbAlarmService.unassignUserAlarms(tbUser.getTenantId(), tbUser, System.currentTimeMillis()); - userService.deleteUser(tenantId, userId); - notificationEntityService.logEntityAction(tenantId, userId, tbUser, customerId, actionType, user, customerId.toString()); + userService.deleteUser(tenantId, user); + notificationEntityService.logEntityAction(tenantId, userId, user, customerId, actionType, responsibleUser, customerId.toString()); } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.USER), - actionType, user, e, userId.toString()); + actionType, responsibleUser, e, userId.toString()); throw e; } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java index 3ef79371d4..0764425116 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java @@ -25,5 +25,5 @@ import javax.servlet.http.HttpServletRequest; public interface TbUserService { User save(TenantId tenantId, CustomerId customerId, User tbUser, boolean sendActivationMail, HttpServletRequest request, User user) throws ThingsboardException; - void delete(TenantId tenantId, CustomerId customerId, User tbUser, User user) throws ThingsboardException; + void delete(TenantId tenantId, CustomerId customerId, User user, User responsibleUser) throws ThingsboardException; } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/InMemoryHouseKeeperServiceService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/InMemoryHouseKeeperServiceService.java new file mode 100644 index 0000000000..4e83aa0060 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/InMemoryHouseKeeperServiceService.java @@ -0,0 +1,104 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.housekeeper; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.housekeeper.HouseKeeperService; +import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +@Component +@RequiredArgsConstructor +@Slf4j +public class InMemoryHouseKeeperServiceService implements HouseKeeperService { + + final TbAlarmService alarmService; + + ListeningExecutorService executor; + + AtomicInteger queueSize = new AtomicInteger(); + AtomicInteger totalProcessedCounter = new AtomicInteger(); + + @PostConstruct + public void init() { + log.debug("Starting HouseKeeper service"); + executor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("housekeeper"))); + } + + @PreDestroy + public void destroy() { + if (executor != null) { + log.debug("Stopping HouseKeeper service"); + executor.shutdown(); + } + } + + @TransactionalEventListener(fallbackExecution = true) + public void handleEvent(DeleteEntityEvent event) { + log.trace("[{}] DeleteEntityEvent handler: {}", event.getTenantId(), event); + EntityId entityId = event.getEntityId(); + if (EntityType.USER.equals(entityId.getEntityType())) { + unassignDeletedUserAlarms(event.getTenantId(), (User) event.getEntity(), event.getTs()); + } + } + + @Override + public ListenableFuture> unassignDeletedUserAlarms(TenantId tenantId, User user, long unassignTs) { + log.debug("[{}][{}] unassignDeletedUserAlarms submitting, pending queue size: {} ", tenantId, user.getId().getId(), queueSize.get()); + queueSize.incrementAndGet(); + ListenableFuture> future = executor.submit(() -> alarmService.unassignDeletedUserAlarms(tenantId, user, unassignTs)); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(List alarmIds) { + queueSize.decrementAndGet(); + totalProcessedCounter.incrementAndGet(); + log.debug("[{}][{}] unassignDeletedUserAlarms finished, pending queue size: {}, total processed count: {} ", + tenantId, user.getId().getId(), queueSize.get(), totalProcessedCounter.get()); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + queueSize.decrementAndGet(); + totalProcessedCounter.incrementAndGet(); + log.error("[{}][{}] unassignDeletedUserAlarms failed, pending queue size: {}, total processed count: {}", + tenantId, user.getId().getId(), queueSize.get(), totalProcessedCounter.get(), throwable); + } + }, MoreExecutors.directExecutor()); + return future; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java index 36c5eacd36..778a97d690 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -46,6 +47,8 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.LinkedList; import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -563,27 +566,28 @@ public class AlarmControllerTest extends AbstractControllerTest { alarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); Assert.assertNotNull(alarm); + AlarmId alarmId = alarm.getId(); - Mockito.reset(tbClusterService, auditLogService); long beforeAssignmentTs = System.currentTimeMillis(); - doPost("/api/alarm/" + alarm.getId() + "/assign/" + savedUser.getId().getId()).andExpect(status().isOk()); - AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + doPost("/api/alarm/" + alarmId.getId() + "/assign/" + savedUser.getId().getId()).andExpect(status().isOk()); + Alarm foundAlarm = doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); Assert.assertEquals(savedUser.getId(), foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); beforeAssignmentTs = System.currentTimeMillis(); - Mockito.reset(tbClusterService, auditLogService); - loginSysAdmin(); doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk()); loginDifferentTenant(); - foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + foundAlarm = Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class), Objects::nonNull); + Assert.assertNotNull(foundAlarm); Assert.assertNull(foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); @@ -609,15 +613,66 @@ public class AlarmControllerTest extends AbstractControllerTest { .build(); alarm = doPost("/api/alarm", alarm, Alarm.class); Assert.assertNotNull(alarm); + AlarmId alarmId = alarm.getId(); + alarm = doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class); + Assert.assertNotNull(alarm); + long beforeAssignmentTs = System.currentTimeMillis(); - alarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + doPost("/api/alarm/" + alarmId.getId() + "/assign/" + savedUser.getId().getId()).andExpect(status().isOk()); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(savedUser.getId(), foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); + + beforeAssignmentTs = System.currentTimeMillis(); + + doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk()); + + foundAlarm = Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class), Objects::nonNull); + + Assert.assertNotNull(foundAlarm); + Assert.assertNull(foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); + } + + @Test + public void testUnassignAlarmOnCustomerRemoving() throws Exception { + createDifferentTenantCustomer(); + loginDifferentTenant(); + + User user = new User(); + user.setAuthority(Authority.CUSTOMER_USER); + user.setTenantId(tenantId); + user.setCustomerId(differentTenantCustomerId); + user.setEmail("customerForAssign@thingsboard.org"); + User savedUser = createUser(user, "password"); + + Device device = createDevice("Different customer device", "default", "differentTenantTest"); + + Device assignedDevice = doPost("/api/customer/" + differentTenantCustomerId.getId() + + "/device/" + device.getId().getId(), Device.class); + Assert.assertEquals(differentTenantCustomerId, assignedDevice.getCustomerId()); + + Alarm alarm = Alarm.builder() + .type(TEST_ALARM_TYPE) + .tenantId(savedDifferentTenant.getId()) + .customerId(differentTenantCustomerId) + .originator(device.getId()) + .severity(AlarmSeverity.MAJOR) + .build(); + alarm = doPost("/api/alarm", alarm, Alarm.class); + Assert.assertNotNull(alarm); + AlarmId alarmId = alarm.getId(); + + alarm = doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class); Assert.assertNotNull(alarm); Mockito.reset(tbClusterService, auditLogService); long beforeAssignmentTs = System.currentTimeMillis(); - doPost("/api/alarm/" + alarm.getId() + "/assign/" + savedUser.getId().getId()).andExpect(status().isOk()); - AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + doPost("/api/alarm/" + alarmId.getId() + "/assign/" + savedUser.getId().getId()).andExpect(status().isOk()); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class); Assert.assertNotNull(foundAlarm); Assert.assertEquals(savedUser.getId(), foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); @@ -626,9 +681,11 @@ public class AlarmControllerTest extends AbstractControllerTest { Mockito.reset(tbClusterService, auditLogService); - doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk()); + doDelete("/api/customer/" + differentTenantCustomerId.getId()).andExpect(status().isOk()); + + foundAlarm = Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class), Objects::nonNull); - foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); Assert.assertNotNull(foundAlarm); Assert.assertNull(foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java index 229ea5d9f9..b3e8941364 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.entitiy.alarm; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.junit.runner.RunWith; @@ -23,14 +24,20 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmId; +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.dao.alarm.AlarmService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.edge.EdgeService; @@ -39,6 +46,8 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; +import java.util.Collections; +import java.util.List; import java.util.UUID; import static org.mockito.ArgumentMatchers.any; @@ -74,7 +83,6 @@ public class DefaultTbAlarmServiceTest { protected TbClusterService tbClusterService; @MockBean private EntitiesVersionControlService vcService; - @SpyBean DefaultTbAlarmService service; @@ -124,4 +132,61 @@ public class DefaultTbAlarmServiceTest { verify(notificationEntityService, times(1)).logEntityAction(any(), any(), any(), any(), eq(ActionType.DELETED), any()); verify(alarmSubscriptionService, times(1)).deleteAlarm(any(), any()); } + + @Test + public void testUnassignAlarm() throws ThingsboardException { + AlarmInfo alarm = new AlarmInfo(); + alarm.setId(new AlarmId(UUID.randomUUID())); + when(alarmSubscriptionService.unassignAlarm(any(), any(), anyLong())) + .thenReturn(AlarmApiCallResult.builder().successful(true).modified(true).alarm(alarm).build()); + + User user = new User(); + user.setEmail("testEmail@gmail.com"); + user.setId(new UserId(UUID.randomUUID())); + service.unassign(new Alarm(), 0L, user); + + ObjectNode commentNode = JacksonUtil.newObjectNode(); + commentNode.put("subtype", "ASSIGN"); + commentNode.put("text", "Alarm was unassigned by user " + user.getTitle()); + commentNode.put("userId", user.getId().getId().toString()); + AlarmComment expectedAlarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(commentNode) + .build(); + + verify(alarmCommentService, times(1)) + .saveAlarmComment(eq(alarm), eq(expectedAlarmComment), eq(user)); + } + + @Test + public void testUnassignDeletedUserAlarms() throws ThingsboardException { + AlarmInfo alarm = new AlarmInfo(); + alarm.setId(new AlarmId(UUID.randomUUID())); + + when(alarmService.findAlarmIdsByAssigneeId(any(), any(), any())) + .thenReturn(new PageData<>(List.of(alarm.getId()), 0, 1, false)) + .thenReturn(new PageData<>(Collections.EMPTY_LIST, 0, 0, false)); + when(alarmSubscriptionService.unassignAlarm(any(), any(), anyLong())) + .thenReturn(AlarmApiCallResult.builder().successful(true).modified(true).alarm(alarm).build()); + + User user = new User(); + user.setEmail("testEmail@gmail.com"); + user.setId(new UserId(UUID.randomUUID())); + service.unassignDeletedUserAlarms(new TenantId(UUID.randomUUID()), user, System.currentTimeMillis()); + + ObjectNode commentNode = JacksonUtil.newObjectNode(); + commentNode.put("subtype", "ASSIGN"); + commentNode.put("text", String.format("Alarm was unassigned because user %s - was deleted", user.getTitle())); + AlarmComment expectedAlarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(commentNode) + .build(); + + verify(alarmCommentService, times(1)) + .saveAlarmComment(eq(alarm), eq(expectedAlarmComment), eq(null)); + } + + } diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index e37e19b382..ccb0b5e26c 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -295,6 +295,7 @@ public class DefaultDeviceStateServiceTest { Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any()); long newTimeout = 1; + Thread.sleep(newTimeout); service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout); activityVerify(false); @@ -330,6 +331,7 @@ public class DefaultDeviceStateServiceTest { Mockito.reset(telemetrySubscriptionService); long newTimeout = 1; + Thread.sleep(newTimeout); service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout); Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any()); 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 fbf8892bd5..558a65e8f4 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 @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.id.EntityId; 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.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -118,6 +119,8 @@ public interface AlarmService extends EntityDaoService { PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); + PageData findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, PageLink pageLink); + void deleteEntityAlarmRelations(TenantId tenantId, EntityId entityId); long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/housekeeper/HouseKeeperService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/housekeeper/HouseKeeperService.java new file mode 100644 index 0000000000..62f53e0f41 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/housekeeper/HouseKeeperService.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.housekeeper; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.List; + +public interface HouseKeeperService { + + ListenableFuture> unassignDeletedUserAlarms(TenantId tenantId, User user, long unassignTs); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index 47a7423191..b874e038d2 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -30,34 +30,34 @@ import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.List; public interface UserService extends EntityDaoService { - - User findUserById(TenantId tenantId, UserId userId); - ListenableFuture findUserByIdAsync(TenantId tenantId, UserId userId); + User findUserById(TenantId tenantId, UserId userId); - User findUserByEmail(TenantId tenantId, String email); + ListenableFuture findUserByIdAsync(TenantId tenantId, UserId userId); + + User findUserByEmail(TenantId tenantId, String email); User findUserByTenantIdAndEmail(TenantId tenantId, String email); - User saveUser(TenantId tenantId, User user); + User saveUser(TenantId tenantId, User user); + + UserCredentials findUserCredentialsByUserId(TenantId tenantId, UserId userId); + + UserCredentials findUserCredentialsByActivateToken(TenantId tenantId, String activateToken); + + UserCredentials findUserCredentialsByResetToken(TenantId tenantId, String resetToken); - UserCredentials findUserCredentialsByUserId(TenantId tenantId, UserId userId); - - UserCredentials findUserCredentialsByActivateToken(TenantId tenantId, String activateToken); + UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials); - UserCredentials findUserCredentialsByResetToken(TenantId tenantId, String resetToken); + UserCredentials activateUserCredentials(TenantId tenantId, String activateToken, String password); - UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials); - - UserCredentials activateUserCredentials(TenantId tenantId, String activateToken, String password); - - UserCredentials requestPasswordReset(TenantId tenantId, String email); + UserCredentials requestPasswordReset(TenantId tenantId, String email); UserCredentials requestExpiredPasswordReset(TenantId tenantId, UserCredentialsId userCredentialsId); UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials); - void deleteUser(TenantId tenantId, UserId userId); + void deleteUser(TenantId tenantId, User user); PageData findUsersByTenantId(TenantId tenantId, PageLink pageLink); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java index 20ed7b549c..43680242e5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.alarm; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -22,6 +23,8 @@ import org.thingsboard.server.common.data.id.UserId; import java.io.Serializable; +import static org.apache.commons.lang3.StringUtils.isNotEmpty; + @Builder @AllArgsConstructor @Data @@ -34,4 +37,22 @@ public class AlarmAssignee implements Serializable { private final String lastName; private final String email; + @JsonIgnore + public String getTitle() { + String title = ""; + if (isNotEmpty(firstName)) { + title += firstName; + } + if (isNotEmpty(lastName)) { + if (!title.isEmpty()) { + title += " "; + } + title += lastName; + } + if (title.isEmpty()) { + title = email; + } + return title; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java index 982e0f94d1..76ea06d27a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java @@ -22,6 +22,7 @@ import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.id.AlarmCommentId; @@ -44,6 +45,7 @@ public class AlarmComment extends BaseData implements HasName { @ApiModelProperty(position = 6, value = "JSON object with text of comment.", dataType = "com.fasterxml.jackson.databind.JsonNode") @NoXss @Length(fieldName = "comment", max = 10000) + @EqualsAndHashCode.Include private transient JsonNode comment; @ApiModelProperty(position = 1, value = "JSON object with the alarm comment Id. " + diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java index 93061d522a..1c0a599082 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java @@ -15,13 +15,15 @@ */ package org.thingsboard.server.transport.coap.efento; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.protobuf.InvalidProtocolBufferException; import lombok.AllArgsConstructor; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.apache.logging.log4j.util.TriConsumer; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Request; -import org.eclipse.californium.core.coap.Response; import org.eclipse.californium.core.network.Exchange; import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.californium.core.server.resources.Resource; @@ -32,8 +34,11 @@ import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransp import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.EfentoCoapDeviceTypeConfiguration; import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.adaptor.ProtoConverter; import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.coap.ConfigProtos; +import org.thingsboard.server.gen.transport.coap.DeviceInfoProtos; import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos; import org.thingsboard.server.gen.transport.coap.MeasurementsProtos; import org.thingsboard.server.transport.coap.AbstractCoapTransportResource; @@ -43,13 +48,17 @@ import org.thingsboard.server.transport.coap.callback.CoapEfentoCallback; import org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils; import java.nio.ByteBuffer; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import static com.google.gson.JsonParser.parseString; import static org.thingsboard.server.transport.coap.CoapTransportService.CONFIGURATION; import static org.thingsboard.server.transport.coap.CoapTransportService.CURRENT_TIMESTAMP; import static org.thingsboard.server.transport.coap.CoapTransportService.DEVICE_INFO; @@ -94,14 +103,13 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { String requestType = uriPath.get(1); switch (requestType) { case MEASUREMENTS: - processMeasurementsRequest(exchange, request); + processMeasurementsRequest(exchange); break; case DEVICE_INFO: + processDeviceInfoRequest(exchange); + break; case CONFIGURATION: - //We respond only to confirmed requests in order to reduce battery consumption for Efento devices. - if (exchange.advanced().getRequest().isConfirmable()) { - exchange.respond(new Response(CoAP.ResponseCode.CREATED)); - } + processConfigurationRequest(exchange); break; default: exchange.respond(CoAP.ResponseCode.BAD_REQUEST); @@ -109,34 +117,92 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { } } - private void processMeasurementsRequest(CoapExchange exchange, Request request) { - byte[] bytes = request.getPayload(); + private void processMeasurementsRequest(CoapExchange exchange) { + byte[] bytes = exchange.advanced().getRequest().getPayload(); try { MeasurementsProtos.ProtoMeasurements protoMeasurements = MeasurementsProtos.ProtoMeasurements.parseFrom(bytes); log.trace("Successfully parsed Efento ProtoMeasurements: [{}]", protoMeasurements.getCloudToken()); - String token = protoMeasurements.getCloudToken(); - transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(token).build(), - new CoapDeviceAuthCallback(exchange, (msg, deviceProfile) -> { - TransportProtos.SessionInfoProto sessionInfo = SessionInfoCreator.create(msg, transportContext, UUID.randomUUID()); - UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); - try { - validateEfentoTransportConfiguration(deviceProfile); - List efentoMeasurements = getEfentoMeasurements(protoMeasurements, sessionId); - transportService.process(sessionInfo, - transportContext.getEfentoCoapAdaptor().convertToPostTelemetry(sessionId, efentoMeasurements), - new CoapEfentoCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - reportSubscriptionInfo(sessionInfo, false, false); - } catch (AdaptorException e) { - log.error("[{}] Failed to decode Efento ProtoMeasurements: ", sessionId, e); - exchange.respond(CoAP.ResponseCode.BAD_REQUEST); - } - })); + validateAndProcessEffentoMessage(protoMeasurements.getCloudToken(), exchange, (deviceProfile, sessionInfo, sessionId) -> { + try { + List measurements = getEfentoMeasurements(protoMeasurements, sessionId); + transportService.process(sessionInfo, + transportContext.getEfentoCoapAdaptor().convertToPostTelemetry(sessionId, measurements), + new CoapEfentoCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + } catch (AdaptorException e) { + log.error("[{}] Failed to decode Efento ProtoMeasurements: ", sessionId, e); + exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + } + }); } catch (Exception e) { log.error("Failed to decode Efento ProtoMeasurements: ", e); exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); } } + private void processDeviceInfoRequest(CoapExchange exchange) { + byte[] bytes = exchange.advanced().getRequest().getPayload(); + try { + DeviceInfoProtos.ProtoDeviceInfo protoDeviceInfo = DeviceInfoProtos.ProtoDeviceInfo.parseFrom(bytes); + String token = protoDeviceInfo.getCloudToken(); + log.trace("Successfully parsed Efento ProtoDeviceInfo: [{}]", token); + validateAndProcessEffentoMessage(token, exchange, (deviceProfile, sessionInfo, sessionId) -> { + try { + EfentoTelemetry deviceInfo = getEfentoDeviceInfo(protoDeviceInfo); + transportService.process(sessionInfo, + transportContext.getEfentoCoapAdaptor().convertToPostTelemetry(sessionId, List.of(deviceInfo)), + new CoapEfentoCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + } catch (AdaptorException e) { + log.error("[{}] Failed to decode Efento ProtoDeviceInfo: ", sessionId, e); + exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + } + }); + } catch (Exception e) { + log.error("Failed to decode Efento ProtoDeviceInfo: ", e); + exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); + } + } + + private void processConfigurationRequest(CoapExchange exchange) { + byte[] bytes = exchange.advanced().getRequest().getPayload(); + try { + ConfigProtos.ProtoConfig protoConfig = ConfigProtos.ProtoConfig.parseFrom(bytes); + String token = protoConfig.getCloudToken(); + log.trace("Successfully parsed Efento ProtoConfig: [{}]", token); + validateAndProcessEffentoMessage(token, exchange, (deviceProfile, sessionInfo, sessionId) -> { + try { + JsonElement configuration = getEfentoConfiguration(bytes); + transportService.process(sessionInfo, + transportContext.getEfentoCoapAdaptor().convertToPostAttributes(sessionId, configuration), + new CoapEfentoCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + } catch (AdaptorException e) { + log.error("[{}] Failed to decode Efento ProtoConfig: ", sessionId, e); + exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + } catch (InvalidProtocolBufferException e) { + throw new RuntimeException(e); + } + }); + } catch (Exception e) { + log.error("Failed to decode Efento ProtoConfig: ", e); + exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); + } + } + + private void validateAndProcessEffentoMessage(String token, CoapExchange exchange, TriConsumer requestProcessor) { + transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(token).build(), + new CoapDeviceAuthCallback(exchange, (msg, deviceProfile) -> { + TransportProtos.SessionInfoProto sessionInfo = SessionInfoCreator.create(msg, transportContext, UUID.randomUUID()); + UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); + try { + validateEfentoTransportConfiguration(deviceProfile); + requestProcessor.accept(deviceProfile, sessionInfo, sessionId); + reportSubscriptionInfo(sessionInfo, false, false); + } catch (AdaptorException e) { + log.error("[{}] Failed to decode Efento request: ", sessionId, e); + exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + } + })); + } + @Override public Resource getChild(String name) { return this; @@ -155,7 +221,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { } } - private List getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) { + private List getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) { String serialNumber = CoapEfentoUtils.convertByteArrayToString(protoMeasurements.getSerialNum().toByteArray()); boolean batteryStatus = protoMeasurements.getBatteryStatus(); int measurementPeriodBase = protoMeasurements.getMeasurementPeriodBase(); @@ -243,6 +309,12 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); values.addProperty("ok_alarm_" + channel, data); break; + case PULSE_CNT: + values = valuesMap.computeIfAbsent(startTimestampMillis, k -> + CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); + values.addProperty("pulse_cnt_" + channel, (double) (startPoint + sampleOffset)); + startTimestampMillis = startTimestampMillis + measurementPeriodMillis; + break; case NO_SENSOR: case UNRECOGNIZED: log.trace("[{}][{}] Sensor error value! Ignoring.", sessionId, measurementTypeName); @@ -261,9 +333,9 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { throw new IllegalStateException("[" + sessionId + "]: Failed to get Efento measurements, reason: channels list is empty!"); } if (!CollectionUtils.isEmpty(valuesMap)) { - List efentoMeasurements = new ArrayList<>(); + List efentoMeasurements = new ArrayList<>(); for (Long ts : valuesMap.keySet()) { - EfentoMeasurements measurement = new EfentoMeasurements(ts, valuesMap.get(ts)); + EfentoTelemetry measurement = new EfentoTelemetry(ts, valuesMap.get(ts)); efentoMeasurements.add(measurement); } return efentoMeasurements; @@ -272,12 +344,107 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { } } + private EfentoTelemetry getEfentoDeviceInfo(DeviceInfoProtos.ProtoDeviceInfo protoDeviceInfo) { + JsonObject values = new JsonObject(); + values.addProperty("sw_version", protoDeviceInfo.getSwVersion()); + + //memory statistics + values.addProperty("nv_storage_status", protoDeviceInfo.getMemoryStatistics(0)); + values.addProperty("timestamp_of_the_end_of_collecting_statistics", getDate(protoDeviceInfo.getMemoryStatistics(1))); + values.addProperty("capacity_of_memory_in_bytes", protoDeviceInfo.getMemoryStatistics(2)); + values.addProperty("used_space_in_bytes", protoDeviceInfo.getMemoryStatistics(3)); + values.addProperty("size_of_invalid_packets_in_bytes", protoDeviceInfo.getMemoryStatistics(4)); + values.addProperty("size_of_corrupted_packets_in_bytes", protoDeviceInfo.getMemoryStatistics(5)); + values.addProperty("number_of_valid_packets", protoDeviceInfo.getMemoryStatistics(6)); + values.addProperty("number_of_invalid_packets", protoDeviceInfo.getMemoryStatistics(7)); + values.addProperty("number_of_corrupted_packets", protoDeviceInfo.getMemoryStatistics(8)); + values.addProperty("number_of_all_samples_for_channel_1", protoDeviceInfo.getMemoryStatistics(9)); + values.addProperty("number_of_all_samples_for_channel_2", protoDeviceInfo.getMemoryStatistics(10)); + values.addProperty("number_of_all_samples_for_channel_3", protoDeviceInfo.getMemoryStatistics(11)); + values.addProperty("number_of_all_samples_for_channel_4", protoDeviceInfo.getMemoryStatistics(12)); + values.addProperty("number_of_all_samples_for_channel_5", protoDeviceInfo.getMemoryStatistics(13)); + values.addProperty("number_of_all_samples_for_channel_6", protoDeviceInfo.getMemoryStatistics(14)); + values.addProperty("timestamp_of_the_first_binary_measurement", getDate(protoDeviceInfo.getMemoryStatistics(15))); + values.addProperty("timestamp_of_the_last_binary_measurement", getDate(protoDeviceInfo.getMemoryStatistics(16))); + values.addProperty("timestamp_of_the_first_binary_measurement_sent", getDate(protoDeviceInfo.getMemoryStatistics(17))); + values.addProperty("timestamp_of_the_first_continuous_measurement", getDate(protoDeviceInfo.getMemoryStatistics(18))); + values.addProperty("timestamp_of_the_last_continuous_measurement", getDate(protoDeviceInfo.getMemoryStatistics(19))); + values.addProperty("timestamp_of_the_last_continuous_measurement_sent", getDate(protoDeviceInfo.getMemoryStatistics(20))); + values.addProperty("nvm_write_counter", protoDeviceInfo.getMemoryStatistics(21)); + + //modem info + DeviceInfoProtos.ProtoModem modem = protoDeviceInfo.getModem(); + values.addProperty("modem_types", modem.getType().toString()); + values.addProperty("sc_EARNFCN_offset", modem.getParameters(0)); + values.addProperty("sc_EARFCN", modem.getParameters(1)); + values.addProperty("sc_PCI", modem.getParameters(2)); + values.addProperty("sc_Cell_id", modem.getParameters(3)); + values.addProperty("sc_RSRP", modem.getParameters(4)); + values.addProperty("sc_RSRQ", modem.getParameters(5)); + values.addProperty("sc_RSSI", modem.getParameters(6)); + values.addProperty("sc_SINR", modem.getParameters(7)); + values.addProperty("sc_Band", modem.getParameters(8)); + values.addProperty("sc_TAC", modem.getParameters(9)); + values.addProperty("sc_ECL", modem.getParameters(10)); + values.addProperty("sc_TX_PWR", modem.getParameters(11)); + values.addProperty("op_mode", modem.getParameters(12)); + values.addProperty("nc_EARFCN", modem.getParameters(13)); + values.addProperty("nc_EARNFCN_offset", modem.getParameters(14)); + values.addProperty("nc_PCI", modem.getParameters(15)); + values.addProperty("nc_RSRP", modem.getParameters(16)); + values.addProperty("RLC_UL_BLER", modem.getParameters(17)); + values.addProperty("RLC_DL_BLER", modem.getParameters(18)); + values.addProperty("MAC_UL_BLER", modem.getParameters(19)); + values.addProperty("MAC_DL_BLER", modem.getParameters(20)); + values.addProperty("MAC_UL_TOTAL_BYTES", modem.getParameters(21)); + values.addProperty("MAC_DL_TOTAL_BYTES", modem.getParameters(22)); + values.addProperty("MAC_UL_total_HARQ_Tx", modem.getParameters(23)); + values.addProperty("MAC_DL_total_HARQ_Tx", modem.getParameters(24)); + values.addProperty("MAC_UL_HARQ_re_Tx", modem.getParameters(25)); + values.addProperty("MAC_DL_HARQ_re_Tx", modem.getParameters(26)); + values.addProperty("RLC_UL_tput", modem.getParameters(27)); + values.addProperty("RLC_DL_tput", modem.getParameters(28)); + values.addProperty("MAC_UL_tput", modem.getParameters(29)); + values.addProperty("MAC_DL_tput", modem.getParameters(30)); + values.addProperty("sleep_duration", modem.getParameters(31)); + values.addProperty("rx_time", modem.getParameters(32)); + values.addProperty("tx_time", modem.getParameters(33)); + + //Runtime info + DeviceInfoProtos.ProtoRuntime runtimeInfo = protoDeviceInfo.getRuntimeInfo(); + values.addProperty("battery_reset_timestamp", getDate(runtimeInfo.getBatteryResetTimestamp())); + values.addProperty("max_mcu_temp", runtimeInfo.getMaxMcuTemperature()); + values.addProperty("mcu_temp", runtimeInfo.getMcuTemperature()); + values.addProperty("counter_of_confirmable_messages_attempts", runtimeInfo.getMessageCounters(0)); + values.addProperty("counter_of_non_confirmable_messages_attempts", runtimeInfo.getMessageCounters(1)); + values.addProperty("counter_of_succeeded_messages", runtimeInfo.getMessageCounters(2)); + values.addProperty("min_battery_mcu_temp", runtimeInfo.getMinBatteryMcuTemperature()); + values.addProperty("min_battery_voltage", runtimeInfo.getMinBatteryVoltage()); + values.addProperty("min_mcu_temp", runtimeInfo.getMinMcuTemperature()); + values.addProperty("runtime_errors", runtimeInfo.getRuntimeErrorsCount()); + values.addProperty("up_time", runtimeInfo.getUpTime()); + + return new EfentoTelemetry(System.currentTimeMillis(), values); + } + + private JsonElement getEfentoConfiguration(byte[] bytes) throws InvalidProtocolBufferException { + return parseString(ProtoConverter.dynamicMsgToJson(bytes, ConfigProtos.getDescriptor().getMessageTypes().get(2))); + } + + private static String getDate(long seconds) { + if (seconds == -1L || seconds == 4294967295L) { + return "Undefined"; + } + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd MMM yyyy HH:mm:ss Z"); + return simpleDateFormat.format(new Date(TimeUnit.SECONDS.toMillis(seconds))); + } + @Data @AllArgsConstructor - public static class EfentoMeasurements { + public static class EfentoTelemetry { private long ts; - private JsonObject values; + private JsonElement values; } } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/adaptor/EfentoCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/adaptor/EfentoCoapAdaptor.java index a6b588139a..ce75608a7d 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/adaptor/EfentoCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/adaptor/EfentoCoapAdaptor.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.coap.efento.adaptor; import com.google.gson.Gson; +import com.google.gson.JsonElement; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.adaptor.AdaptorException; @@ -32,12 +33,23 @@ public class EfentoCoapAdaptor { private static final Gson gson = new Gson(); - public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, List measurements) throws AdaptorException { + public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, List telemetryList) throws AdaptorException { try { - return JsonConverter.convertToTelemetryProto(gson.toJsonTree(measurements)); + return JsonConverter.convertToTelemetryProto(gson.toJsonTree(telemetryList)); } catch (Exception ex) { log.warn("[{}] Failed to convert EfentoMeasurements to PostTelemetry request!", sessionId); throw new AdaptorException(ex); } } + + public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, JsonElement deviceInfo) throws AdaptorException { + try { + return JsonConverter.convertToAttributesProto(deviceInfo); + } catch (Exception ex) { + log.warn("[{}] Failed to convert JsonObject to PostTelemetry request!", sessionId); + throw new AdaptorException(ex); + } + } + + } diff --git a/common/transport/coap/src/main/proto/efento/proto_config.proto b/common/transport/coap/src/main/proto/efento/proto_config.proto new file mode 100644 index 0000000000..0903494245 --- /dev/null +++ b/common/transport/coap/src/main/proto/efento/proto_config.proto @@ -0,0 +1,312 @@ +/** + * Copyright © 2016-2023 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. + */ +syntax = "proto3"; + +import "efento/proto_measurement_types.proto"; +import "efento/proto_rule.proto"; + +option java_package = "org.thingsboard.server.gen.transport.coap"; +option java_outer_classname = "ConfigProtos"; + +/* Message containing optional channels control parameters */ +message ProtoChannelControl { + + /* Channel index */ + uint32 channel_index = 1; + + /* Control parameters. Maximal number equals 4. This field is channel specific: */ + /* IO_control channel: */ + /* - control_params[0]: */ + /* - Byte 0: On state configuration */ + /* 0x01 - Low */ + /* 0x02 - High */ + /* 0x03 - High-Z (disconnected) */ + /* - Byte 1: Off state configuration */ + /* 0x01 - Low */ + /* 0x02 - High */ + /* 0x03 - High-Z (disconnected) */ + /* - Byte 2: Power on channel state */ + /* 0x01 - On */ + /* 0x02 - Off */ + repeated uint32 control_params = 2; +} + +/* Message containing request data for accesing calibration parameters */ +message ProtoCalibrationParameters { + + /* Request details. Bitmask: */ + /* - calibration_request[0:2] - requested channel number. */ + uint32 calibration_request = 1; + + /* Assignment of a channel. */ + uint32 channel_assignment = 2; + + /* Table of calibration parameters. Max size = 8. */ + repeated int32 parameters = 3; +} + +/* Main message sent in the payload. Each field in this message is independent of the others - only parameters that should be */ +/* changed need to be sent in the payload. */ +/* If the value of a selected parameter shall not be changed, do not include it in the payload */ +message ProtoConfig { + + /* DEPRECATED - Used for backward compatibility with fw versions 5.x */ + /* repeated Threshold thresholds = 1; */ + + /* 'Measurement_period_base' and 'measurement_period_factor' define how often the measurements are taken. */ + /* Sensors of 'Continuous' type take measurement each Measurement_period_base * measurement_period_factor. */ + /* Sensors of 'Binary' type take measurement each Measurement_period_base. */ + /* For backward compatibility with versions 5.x in case of binary/mixed sensors, if the 'measurement_period_factor' is */ + /* not sent (equal to 0), then the default value '14' shall be used for period calculation. */ + /* For backward compatibility with versions 5.x in case of continues sensors, if the measurement_period_factor is */ + /* not sent (equal to 0), then the default value '1' shall be used for period calculation. */ + /* measurement period base in seconds */ + /* Range [1:65535] - minimum value can vary depends on installed sensors */ + uint32 measurement_period_base = 2; + + /* Measurement period factor */ + /* Range [1:65535] - minimum value can vary depends on installed sensors */ + uint32 measurement_period_factor = 26; + + /* Transmission interval in seconds. Range: [60:604800] */ + uint32 transmission_interval = 3; + + /* BLE turnoff time in seconds. Once receiving this setting, BLE will be switched off after the set number of seconds. */ + /* If BLE is already switched off, it will switch on for the set number of seconds and switch off afterwards. */ + /* Range [60:604800] and 0xFFFFFFFF */ + /* 0xFFFFFFFF - always on */ + uint32 ble_turnoff_time = 4; + + /* ACK interval in seconds */ + /* Range [180:2592000] and 0xFFFFFFFF */ + /* 0xFFFFFFFF - always request ACK */ + uint32 ack_interval = 5; + + /* Specifies, if the additional device info is requested. If true, sensor will send a message to endpoint '/i' with the */ + /* device info. This field is only sent by server */ + bool request_device_info = 6; + + /* Specifies, if software update is available. This field is only sent by server */ + bool request_fw_update = 7; + + /* Current time in seconds sine 1st of January 1970 (epoch time). */ + uint32 current_time = 8; + + /* NB-IoT transfer limit */ + /* Range: [1:65535] */ + /* 65535 - disable transfer limit function */ + uint32 transfer_limit = 9; + + /* NB-IoT transfer limit timer in seconds */ + /* Range: [1:65535] */ + /* 65535 - disable transfer limit function */ + uint32 transfer_limit_timer = 10; + + /* Data (measurements) server IP address */ + /* IP of the data server as string in form x.x.x.x. For example: 18.184.24.239 */ + string data_server_ip = 11; + + /* Data (measurements) server port */ + /* Range: [1:65535] */ + uint32 data_server_port = 12; + + /* Update server IP address */ + /* IP of data server as string in form x.x.x.x. For example: 18.184.24.239 */ + string update_server_ip = 13; + + /* Update server port for UDP transmission */ + /* Range: [1:65535] */ + uint32 update_server_port_udp = 14; + + /* Update server port for CoAP transmission */ + /* Range: [1:65535] */ + uint32 update_server_port_coap = 15; + + /* APN as string. Max length 49 */ + /* String with special character 0x7F (DEL) only indicates that automatic apn is turn on */ + string apn = 16; + + /* PLMN selection */ + /* Range: [100:999999] */ + /* 0xFFFFFFFF or 1000000 - automatic selection */ + uint32 plmn_selection = 17; + + /* Device will power off its cellular modem for requested number of seconds. Maximum number of seconds 604800 (7 days) */ + /* This field is only sent by server */ + uint32 disable_modem_request = 18; + + /* If set, the device will send its configuration to the endpoint '/c' as a confirmable message */ + /* This field is only sent by server */ + bool request_configuration = 19; + + /* Device's error codes. */ + /* This field is only sent by device */ + repeated uint32 errors = 20; + + /* Identifier of current configuration - Every change of the configuration results in change of the value of this field */ + /* This field is only sent by device */ + uint32 hash = 21; + + /* If true, the device will accept the configuration without functional testing (eg. network connection) */ + bool accept_without_testing = 22; + + /* Cloud token configuration: */ + /* - 1: cloud token set to the value of cloud_token field */ + /* - 2: cloud token set to IMEI of the cellular module */ + /* - 255: do not send cloud_token field */ + uint32 cloud_token_config = 23; + + /* Cloud token that should be sent with each measurement frame */ + string cloud_token = 24; + + /* Serial number of the device */ + /* This field is only sent by device */ + bytes serial_number = 25; + + /* Type of channel */ + /* This field is only sent by device */ + repeated MeasurementType channel_types = 27; + + /* Edge logic rules set on the device. Up to 12 rules are supported */ + repeated ProtoRule rules = 28; + + /* Supervision period */ + /* Range: [180:604800] */ + /* 0xFFFFFFFF - Functionality disabled */ + uint32 supervision_period = 29; + + /* If true, sensor's measurement memory will be erased */ + bool memory_reset_request = 30; + + /* Bytes 0-4 - Band selection mask. Mask = 1 << position */ + /* Band | 1 | 2 | 3 | 4 | 5 | 8 | 12 | 13 | 17 | 18 | 19 | 20 | 25 | 26 | 28 | 66 | 71 | 85 | */ + /* Position: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | */ + /* example: To enable band 3, 8 and 20 set to (1 << 2) + (1 << 5) + (1 << 11) = 2084 */ + uint32 modem_bands_mask = 31; + + /* Data endpoint (string - max length 16) */ + string data_endpoint = 32; + + /* Configuration endpoint (string - max length 16) */ + string configuration_endpoint = 33; + + /* Device info endpoint (string - max length 16) */ + string device_info_endpoint = 34; + + /* Time endpoint (string - max length 16) */ + string time_endpoint = 35; + + /* Bluetooth TX power level. Value is the index of the absolute value of TX power, that depends on the BLE module */ + /* Range: [1:4] */ + uint32 ble_tx_power_level = 36; + + /* Deprecated field */ + /* If true, the sensor's runtime errors will be cleared */ + bool request_runtime_errors_clear = 37; + + /* Timestamp when a new error code was reported */ + uint32 error_timestamp = 38; + + /* Timestamp when the new configuration was set */ + uint32 hash_timestamp = 39; + + /* Cloud token CoAP option ID: */ + /* - [1:64999] - CoAP option ID containing cloud token */ + /* - 65000 - cloud token sent in the payload */ + uint32 cloud_token_coap_option = 40; + + /* ECDSA payload signature CoAP option ID: */ + /* - [1:64999] - CoAP option ID containing ECDSA payload signature */ + /* - 65000 - no payload signature in CoAP option */ + uint32 payload_signature_coap_option = 41; + + /* DNS server IP address grouped in the array as four octets. Set 255.255.255.255 to use a network DNS server */ + /* Note: when setting less than four octets the remaining will be filled with zeros. */ + repeated uint32 dns_server_ip = 42; + + /* DNS TTL configuration: */ + /* - [1:864000] - custom TTL in seconds (additionally, the DNS request when communication has failed) */ + /* - 864001 - accept TTL from the DNS server (additionally, the DNS request when communication has failed) */ + /* - 864002 - DNS request is only after communication failed */ + uint32 dns_ttl_config = 43; + + /* Configuration payload split information. Information about dividing the payload into parts */ + /* values < 0 - payload has been split, expect another part of the payload in the next message. */ + /* The absolute value indicates an index of the current message. */ + /* value = 0 - payload has not been splitted */ + /* values > 0 - last part of the split payload, the value indicates the total number of the messages sent */ + sint32 payload_split_info = 44; + + /* Modem update request (string - max length 48) */ + /* This field is only sent by server */ + /* For BC66 module, this field is a DFOTA URL */ + string modem_update_request = 45; + + /* Cellular configuration parameters. */ + /* 1st item - Number of used cellular parameters */ + /* 2nd - 12th items - Cellular parameters */ + repeated uint32 cellular_config_params = 46; + + /* Calendar configuration. Up to 6 calendars are supported */ + repeated ProtoCalendar calendars = 47; + + /* Control parameters for channels. Maximal number of requests equals 6 */ + /* This field is only sent by server */ + repeated ProtoChannelControl channels_control_request = 48; + + /* Set/get calibration parameters for single channel. */ + ProtoCalibrationParameters calibration_parameters_request = 49; + + /* LED behaviour configuration: */ + /* Period of LEDs flashing (5-600 seconds in 5 seconds resolution): */ + /* - led_config[0] - green LED */ + /* - led_config[1] - red LED */ + /* Time from entering the normal state, after which the LED indication is turned off */ + /* (0-240 minutes in 1 minute resolution, or 255 for always turned on): */ + /* - led_config[2] - flashing red led on communication problem */ + /* - led_config[3] - flashing red led on a sensor problem */ + /* - led_config[4] - flashing red led on a low power */ + /* - led_config[5] - flashing green led on measurement */ + /* - led_config[6] - flashing green led on transmission */ + /* - led_config[7] - flashing green led to indicate sensor's proper operation */ + /* - led_config[8] - Blink duration (20-1000ms in 5 ms resolution) */ + repeated uint32 led_config = 50; + + /* Network troubleshooting configuration, if bluetooth is turned off and communication with the server is faulty, */ + /* bluetooth will be automatically turned on until the connection is stabilized */ + /* - 1: network troubleshooting disabled */ + /* - 2: network troubleshooting enabled */ + uint32 network_troubleshooting = 51; + + /* Reserved by gateway client */ + reserved 52, 53; + + /* Encryption key configuration. Sensor sends in this field two last bytes of SHA256 hash calculated from its current */ + /* encryption_key configuration. */ + /* Max length: 16 bytes. */ + /* 0x7F - encryption key disabled. */ + bytes encryption_key = 54; + + /* User name as string. Max length 31 */ + /* String with special character 0x7F (DEL) only indicates that automatic user name is turn on */ + /* User name can only be set to custom value if apn has been configured (is not automatic) */ + string apn_user_name = 55; + + /* Password as string. Max length 31 */ + /* String with special character 0x7F (DEL) only indicates that automatic password is turn on */ + /* Password can only be set to custom value if apn_user_name has been configured (is not automatic) */ + string apn_password = 56; +} \ No newline at end of file diff --git a/common/transport/coap/src/main/proto/efento/proto_device_info.proto b/common/transport/coap/src/main/proto/efento/proto_device_info.proto new file mode 100644 index 0000000000..27dc73c25b --- /dev/null +++ b/common/transport/coap/src/main/proto/efento/proto_device_info.proto @@ -0,0 +1,197 @@ +/** + * Copyright © 2016-2023 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. + */ +syntax = "proto3"; + +option java_package = "org.thingsboard.server.gen.transport.coap"; +option java_outer_classname = "DeviceInfoProtos"; + +enum ModemType +{ + + /* Invalid value */ + MODEM_TYPE_UNSPECIFIED = 0; + + /* Quectel BC66 modem */ + MODEM_TYPE_BC66 = 1; + + /* Quectel BC66-NA modem */ + MODEM_TYPE_BC66NA = 2; +} + +message ProtoRuntime +{ + + /* Up-time in seconds (since reset) */ + uint32 up_time = 1; + + /* Message counters (since reset). There are 3 counters: */ + /* message_counters[0] - Counter of confirmable messages attempts */ + /* message_counters[1] - Counter of non-confirmable messages attempts */ + /* message_counters[2] - Counter of succeeded messages */ + repeated uint32 message_counters = 2; + + /* MCU temperature in Celsius */ + sint32 mcu_temperature = 3; + + /* Minimum battery voltage in mV */ + uint32 min_battery_voltage = 4; + + /* MCU temperature in Celsius, while the minimum battery voltage was reached */ + sint32 min_battery_mcu_temperature = 5; + + /* Battery reset timestamp (Unix timestamp) */ + uint32 battery_reset_timestamp = 6; + + /* Max MCU temperature in Celsius */ + sint32 max_mcu_temperature = 7; + + /* Min MCU temperature in Celsius */ + sint32 min_mcu_temperature = 8; + + /* Table of runtime errors. Max length: 20 */ + repeated uint32 runtime_errors = 9; +} + +message ProtoModem +{ + + ModemType type = 1; + + /* Parameters for BC66 modem: */ + /* parameters[0] - sc_EARFCN - Range: [0:262143]. Unknown value: -1 */ + /* parameters[1] - sc_EARNFCN_offset - Range: [0:4] mapped to [-2, -1, -0.5, 0, 1]. Unknown value: -1 */ + /* parameters[2] - sc_PCI - Range: [0:502]. Unknown value: -1 */ + /* parameters[3] - sc_Cell id - Range: [1:268435456]. Unknown value: 0 */ + /* parameters[4] - sc_RSRP - [dBm] - Range: [-140:-44]. Unknown value: 0 */ + /* parameters[5] - sc_RSRQ - [dB] - Range: [-20:-3]. Unknown value: 0 */ + /* parameters[6] - sc_RSSI - [dBm] - Range: [-110:-3] Unknown value: 0 */ + /* parameters[7] - sc_SINR - [dB] - Range: [-10:30]. Unknown value: 31 */ + /* parameters[8] - sc_Band - Range: [see module supported bands]. The current serving cell band. Unknown value: -1 */ + /* parameters[9] - sc_TAC - Range: [0:65536]. Unknown value: -1 */ + /* parameters[10] - sc_ECL - Range: [0:2]. Unknown value: -1 */ + /* parameters[11] - sc_TX_PWR - [0.1cBm] - Range [-440:230]. Unknown value: -1000 */ + /* parameters[12] - OP_MODE - Range: [0:3]. Unknown value: -1 */ + /* parameters[13] - nc_EARFCN - Range: [0:262143]. Unknown value: -1 */ + /* parameters[14] - nc_EARNFCN_offset - Range: [0:4] mapped to [-2, -1, -0.5, 0, 1]. Unknown value: -1 */ + /* parameters[15] - nc_PCI - Range: [0:502]. Unknown value: -1 */ + /* parameters[16] - nc_RSRP - [dBm] - Range: [-140:-44]. Unknown value: 0 */ + /* parameters[17] - RLC_UL_BLER - Range: [0:100]. Unknown value: -1 */ + /* parameters[18] - RLC_DL_BLER - Range: [0:100]. Unknown value: -1 */ + /* parameters[19] - MAC_UL_BLER - Range: [0:100]. Unknown value: -1 */ + /* parameters[20] - MAC_DL_BLER - Range: [0:100]. Unknown value: -1 */ + /* parameters[21] - MAC_UL_TOTAL_BYTES - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[22] - MAC_DL_TOTAL_BYTES - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[23] - MAC_UL_total_HARQ_Tx - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[24] - MAC_DL_total_HARQ_Tx - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[25] - MAC_UL_HARQ_re_Tx - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[26] - MAC_DL_HARQ_re_Tx - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[27] - RLC_UL_tput - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[28] - RLC_DL_tput - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[29] - MAC_UL_tput - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[30] - MAC_DL_tput - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[31] - sleep_duration - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[32] - Rx_time - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */ + /* parameters[33] - Tx_time - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */ + repeated sint32 parameters = 2; +} + +message ProtoUpdateInfo +{ + + /* Timestamp of update (Unix timestamp) */ + uint32 timestamp = 1; + + /* Update status, possible values: */ + /* - 0x1 - No update yet */ + /* - 0x2 - No error */ + /* - 0x3 - UDP socekt error */ + /* - 0x4 - Hash error */ + /* - 0x5 - Missing packet error */ + /* - 0x6 - Invalid data error */ + /* - 0x7 - Sending timeout error */ + /* - 0x8 - No SW to update error */ + /* - 0x9 - Sending unexpected error */ + /* - 0x10 - Unexpected error */ + uint32 status = 2; +} + +message ProtoDeviceInfo +{ + + /* Serial number of device */ + bytes serial_num = 1; + + /* Deprecated field */ + reserved 2; + + /* Software version e.g ver 06.10 -> 0x060A -> 1546 */ + uint32 sw_version = 3; + + /* Deprecated fields */ + reserved 4,5,6,7,8,9,10,11,12; + + /* Structure with battery and temperature information */ + ProtoRuntime runtime_info = 13; + + /* Structure with modem specific runtime information */ + ProtoModem modem = 14; + + /* String up to 7 bytes long. Software commit id e.g. "e0e8556" */ + /* From version 06.07 the first two characters indicate the LTS version. */ + /* For example: the value "0bdd23f" means LTS version 11 and the beginning of the commit ID "dd23f" */ + string commit_id = 15; + + /* Optional string up to 36 bytes long. Can be set to any user define value or hold device's IMEI */ + string cloud_token = 16; + + /* Memory statistics: */ + /* memory_statistics[0] - Status of Nv storage: */ + /* - 0 - Nv storage hasn't errors */ + /* - 1 - Nv storage has some corrupted packet. Memory is read-only */ + /* - 2 - Nv storage is corrupted. Memory is unavailable */ + /* memory_statistics[1] - Timestamp of the end of collecting statistics. */ + /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */ + /* memory_statistics[2] - Capacity of memory in bytes */ + /* memory_statistics[3] - Used space in bytes */ + /* memory_statistics[4] - Size of invalid (outdated) packets in bytes */ + /* memory_statistics[5] - Size of corrupted packets in bytes */ + /* memory_statistics[6] - Number of valid packets */ + /* memory_statistics[7] - Number of invalid (outdated) packets */ + /* memory_statistics[8] - Number of corrupted packets */ + /* memory_statistics[9] - Number of all samples for channel 1 (valid packets) */ + /* memory_statistics[10] - Number of all samples for channel 2 (valid packets) */ + /* memory_statistics[11] - Number of all samples for channel 3 (valid packets) */ + /* memory_statistics[12] - Number of all samples for channel 4 (valid packets) */ + /* memory_statistics[13] - Number of all samples for channel 5 (valid packets) */ + /* memory_statistics[14] - Number of all samples for channel 6 (valid packets) */ + /* memory_statistics[15] - Timestamp of the first binary measurement. */ + /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */ + /* memory_statistics[16] - Timestamp of the last binary measurement. */ + /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */ + /* memory_statistics[17] - Timestamp of the last binary measurement, that marked as sent. */ + /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */ + /* memory_statistics[18] - Timestamp of the first continuous measurement. */ + /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */ + /* memory_statistics[19] - Timestamp of the last continuous measurement. */ + /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */ + /* memory_statistics[20] - Timestamp of the last continuous measurement, that marked as sent. */ + /* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */ + /* memory_statistics[21] - NVM write counter */ + repeated uint32 memory_statistics = 17; + + /* Information about last sensor SW update */ + ProtoUpdateInfo last_update_info = 18; +} \ No newline at end of file diff --git a/common/transport/coap/src/main/proto/proto_measurement_types.proto b/common/transport/coap/src/main/proto/efento/proto_measurement_types.proto similarity index 100% rename from common/transport/coap/src/main/proto/proto_measurement_types.proto rename to common/transport/coap/src/main/proto/efento/proto_measurement_types.proto diff --git a/common/transport/coap/src/main/proto/proto_measurements.proto b/common/transport/coap/src/main/proto/efento/proto_measurements.proto similarity index 99% rename from common/transport/coap/src/main/proto/proto_measurements.proto rename to common/transport/coap/src/main/proto/efento/proto_measurements.proto index 27bd6e3313..fd93b38b82 100644 --- a/common/transport/coap/src/main/proto/proto_measurements.proto +++ b/common/transport/coap/src/main/proto/efento/proto_measurements.proto @@ -14,7 +14,7 @@ * limitations under the License. */ syntax = "proto3"; -import "proto_measurement_types.proto"; +import "efento/proto_measurement_types.proto"; option java_package = "org.thingsboard.server.gen.transport.coap"; option java_outer_classname = "MeasurementsProtos"; diff --git a/common/transport/coap/src/main/proto/efento/proto_rule.proto b/common/transport/coap/src/main/proto/efento/proto_rule.proto new file mode 100644 index 0000000000..9a4a3cd0d1 --- /dev/null +++ b/common/transport/coap/src/main/proto/efento/proto_rule.proto @@ -0,0 +1,267 @@ +/** + * Copyright © 2016-2023 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. + */ +syntax = "proto3"; + +option java_package = "org.thingsboard.server.gen.transport.coap"; +option java_outer_classname = "ProtoRuleProtos"; + +/* Encoding A: used to set absolute values in the Rules (e.g. upper and lower threshold values) */ +/* - TEMPERATURE - [°C] - Celsius degree. Resolution 0.1°C. Range [-273.2:4000.0]. */ +/* - HUMIDITY - [% RH] - Relative humidity. Resolution 1%. Range [0:100]. */ +/* - ATMOSPHERIC_PRESSURE - [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [1.0:2000.0]. */ +/* - DIFERENTIAL_PRESSURE - [Pa] - Pascal. Resolution 1Pa. Range [-10000:10000] */ +/* - OK/ALARM - Not applicable */ +/* - IAQ - [IAQ] - IAQ index. Resolution 1IAQ. Range [0:500]. */ +/* - FLOODING - Not applicable */ +/* - PULSE_CNT - [NB] Number of pulses. Resolution 1 pulse. Range [0:8000000]. */ +/* - ELECTRICITY_METER - [W] - Watt; Resolution 1W. Range [0:8000000]. Average power consumption in period */ +/* - WATER_METER [l/min] - Liter per minute. Resolution 1l/min. Range [0:8000000]. Average water flow in period. */ +/* - SOIL_MOISTURE - [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [-1000:0]. Soil moisture (tension). */ +/* - CO_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon monoxide concentration. */ +/* - NO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Nitrogen dioxide concentration. */ +/* - H2S_GAS - [ppm] - Parts per million. Resolution 0.01ppm. Range [0.00:80000.00]. Hydrogen sulfide concentration. */ +/* - AMBIENT_LIGHT -[lx] - Lux. Resolution 0.1lx. Range [0.0:100000.0]. Illuminance. */ +/* - PM_1_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0:1000]. */ +/* - PM_2_5 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0:1000]. */ +/* - PM_10_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0:1000]. */ +/* - NOISE_LEVEL - [dB] - Decibels. Resolution 0.1 dB. Range: [0.0:200.0]. Noise level. */ +/* - NH3_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Ammonia concentration. */ +/* - CH4_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Methane concentration. */ +/* - HIGH_PRESSURE - [kPa] - Kilopascal (1kPa = 1000Pa, 100kPa = 1bar). Resolution 1kPa. Range [0:200000]. Pressure. */ +/* - DISTANCE_MM - [mm] - Millimeter. Resolution 1mm. Range [0:100000]. Distance. */ +/* - WATER_METER_ACC_MINOR - [l] - Liter. Resolution 1l. Range [0:1000000]. Accumulative water meter (minor). */ +/* - WATER_METER_ACC_MAJOR - [hl] - Hectoliter. Resolution 1hl. Range [0:1000000]. Accumulative water meter (major). */ +/* - CO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon dioxide concentration. */ +/* - HUMIDITY ACCURATE - [% RH] - Relative humidity. Resolution 0.1%. Range [0.0:100.0]. */ +/* - STATIC_IAQ - [sIAQ] - Static IAQ index. Resolution 1sIAQ. Range [0:10000]. */ +/* - CO2_EQUIVALENT - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon dioxide equivalent. */ +/* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [0:100000]. Breath VOC estimate. */ +/* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.00:100.00]. */ +/* - PULSE_CNT_ACC_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [0:1000000]. Accumulative pulse counter (minor). */ +/* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [0:1000000]. */ +/* Accumulative pulse counter (major). */ +/* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [0:1000000]. Accumulative electricity meter (minor). */ +/* - ELEC_METER_ACC_MAJOR - [kWh] - Kilowatt-hour. Resolution 1kWh. Range [0:1000000]. Accumulative electricity meter (major). */ + +/* Encoding R: used to set relative values in the Rules (e.g. differential threshold and hysteresis) */ +/* - TEMPERATURE - [°C] - Celsius degree. Resolution 0.1°C. Range [0.1:4273.2]. */ +/* - HUMIDITY - [% RH] - Relative humidity. Resolution 1%. Range [1:100]. */ +/* - ATMOSPHERIC_PRESSURE - [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [0.1:1999.0]. */ +/* - DIFERENTIAL_PRESSURE - [Pa] - Pascal. Resolution 1Pa. Range [1:20000] */ +/* - OK/ALARM - Not applicable */ +/* - VOC - [IAQ] - Iaq index. Resolution 1IAQ. Range [1:500]. */ +/* - FLOODING - Not applicable */ +/* - PULSE_CNT - [NB] Number of pulses. Resolution 1 pulse. Range [1:8000000]. */ +/* - ELECTRICITY_METER - [W] - Watt; Resolution 1W. Range [1:8000000]. Average power consumption in period */ +/* - WATER_METER [l/min] - Liter per minute. Resolution 1l/min. Range [1:8000000]. Average water flow in period. */ +/* - SOIL_MOISTURE - [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [1:1000]. Soil moisture (tension). */ +/* - CO_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Carbon monoxide concentration. */ +/* - NO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Nitrogen dioxide concentration. */ +/* - H2S_GAS - [ppm] - Parts per million. Resolution 0.01ppm. Range [0.01:80000.00]. Hydrogen sulfide concentration. */ +/* - AMBIENT_LIGHT -[lx] - Lux. Resolution 0.1lx. Range [0.1:100000.0]. Illuminance. */ +/* - PM_1_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [1:1000]. */ +/* - PM_2_5 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [1:1000]. */ +/* - PM_10_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [1:1000]. */ +/* - NOISE_LEVEL - [dB] - Decibels. Resolution 0.1 dB. Range: [0.1:200.0]. Noise level. */ +/* - NH3_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Ammonia concentration. */ +/* - CH4_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Methane concentration. */ +/* - HIGH_PRESSURE - [kPa] - Kilopascal (1kPa = 1000Pa, 100kPa = 1bar). Resolution 1kPa. Range [1:200000]. Pressure. */ +/* - DISTANCE_MM - [mm] - Millimeter. Resolution 1mm. Range [1:100000]. Distance. */ +/* - WATER_METER_ACC_MINOR - [l] - Liter. Resolution 1l. Range [1:1000000]. Accumulative water meter (minor). */ +/* - WATER_METER_ACC_MAJOR - [hl] - Hectoliter. Resolution 1hl. Range [1:1000000]. Accumulative water meter (major). */ +/* - CO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Carbon dioxide concentration. */ +/* - HUMIDITY ACCURATE - [% RH] - Relative humidity. Resolution 0.1%. Range [0.1:100.0]. */ +/* - STATIC_IAQ - [sIAQ] - Static IAQ index. Resolution 1sIAQ. Range [1:10000]. */ +/* - CO2_EQUIVALENT - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Carbon dioxide equivalent. */ +/* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [1:100000]. Breath VOC estimate. */ +/* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.01:100.00]. */ +/* - PULSE_CNT_ACC_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [1:1000000]. Accumulative pulse counter (minor). */ +/* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [1:1000000]. */ +/* Accumulative pulse counter (major). */ +/* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [1:1000000]. Accumulative electricity meter (minor). */ +/* - ELEC_METER_ACC_MAJOR - [kWh] - Kilowatt-hour. Resolution 1kWh. Range [1:1000000]. Accumulative electricity meter (major). */ + +/* Condition to be checked by the device. If the condition is true, an action is triggered */ +enum Condition { + + /* Invalid value */ + CONDITION_UNSPECIFIED = 0; + + /* Threshold function for given rule_id is disabled */ + CONDITION_DISABLED = 1; + + /* Upper threshold. Continuous sensors only. If the measurement (or average from a few measurements) is over the threshold, */ + /* an action is triggered. */ + /* parameter[0] - Threshold value in "Encoding A" format. Must match channel type */ + /* parameter[1] - Hysteresis value in "Encoding R" format. Must much channel type. Set to "0" to disable */ + /* parameter[2] - Triggering mode: */ + /* - 1 - moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) */ + /* - 2 - window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) */ + /* - 3 - consecutive samples (number of consecutive samples above threshold) */ + /* parameter[3] - Number of measurements for trigger determination. E.g parameter[3] equals 3, average value from three */ + /* samples will be calculated and compared to the threshold value in average mode or the third consecutive */ + /* sample above threshold will trigger action in consecutive mode. Range: [1:10]. */ + /* parameter[4] - Type of measurement (as described in MeasurementType). */ + CONDITION_HIGH_THRESHOLD = 2; + + /* Lower threshold. Continuous sensors only. If the measurement (or average from a few measurements) is below the threshold, */ + /* an action is triggered. */ + /* parameter[0] - Threshold value in "Encoding A" format. Must match channel type */ + /* parameter[1] - Hysteresis value in "Encoding R" format. Must much channel type. Set to "0" to disable */ + /* parameter[2] - Triggering mode: */ + /* - 1 - moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) */ + /* - 2 - window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) */ + /* - 3 - consecutive samples (number of consecutive samples above threshold) */ + /* parameter[3] - Number of measurements for trigger determination. E.g parameter[3] equals 3, average value from three */ + /* samples will be calculated and compared to the threshold value in average mode or the third consecutive */ + /* sample below threshold will trigger action in consecutive mode. Range: [1:10]. */ + /* parameter[4] - Type of measurement (as described in MeasurementType). */ + CONDITION_LOW_THRESHOLD = 3; + + /* Differential threshold. Continuous sensors only. If the absolute value of the difference between the last value sent to */ + /* the server and the measurement value (or average from a few measurements) is greater or equal to the value of */ + /* the threshold set, an action is triggered. */ + /* parameter[0] - Threshold value in "Encoding R" format. Must match channel type */ + /* parameter[1] - Triggering mode: */ + /* - 1 - moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) */ + /* - 2 - window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) */ + /* - 3 - consecutive samples (number of consecutive samples above threshold) */ + /* parameter[2] - Number of measurements for trigger determination. E.g parameter[3] equals 3, average value from three */ + /* samples will be calculated and compared to the threshold value in average mode or the third consecutive */ + /* sample exceeding threshold will trigger action in consecutive mode. Range: [1:10]. */ + /* parameter[3] - Type of measurement (as described in MeasurementType). */ + CONDITION_DIFF_THRESHOLD = 4; + + /* Change of binary sensor's state. Binary sensors only. Each change of the binary's sensor state will trigger an action. */ + CONDITION_BINARY_CHANGE_STATE = 5; + + /* Logic operator. Used for combining multiple rules into more complex conditions. If the logic condition specified by */ + /* parameters (logic operator and selected rules) is met, an action is triggered. */ + /* parameter[0] - Logic operator (as described in LogicOperation). */ + /* parameter[1] - Rule selector (bit mask). Specifies which rules should be taken into account while determining */ + /* rules outcome. */ + /* parameter[2] - Rule negation (bit mask). Specifies which of chosen in parameter[1] rules should be negated */ + /* before determining rules outcome. */ + /* parameter[3] - Rule action delay [s]. Specifies time delay between the rule activation and rule action being triggered. */ + /* Range: [0:864000]. */ + /* parameter[4] - Rule return delay [s]. Specifies time delay between the rule deactivation and rule action being triggered. */ + /* Range: [0:864001]. Max parameter value disables action triggering on rule deactivation. */ + CONDITION_LOGIC_OPERATOR = 6; + + /* On measurement. Continous sensors only. The basic function is to trigger communication after measurement if at least 60s */ + /* have passed since the last one. Transmission may occur every x measurement. Optionally dependency on the other rule can */ + /* be configured, then, when all conditions are met, transmission is triggered. */ + /* parameter[0] - Send every n measurement. This parameter specifies every which measurement transmission will be triggered */ + /* if all other conditions are met. Range: [1:500]. If parameter[0] equals 1, transmission will occur after */ + /* every measurement. */ + /* parameter[1] - Optional. Rule selector (bit mask). Specifies which rule should be taken into account while determining */ + /* the measurement rule outcome. */ + /* parameter[2] - Optional. Rule negation (bit mask). Specifies which of chosen in parameter[1] rule should be negated */ + /* before determining the measurement rule outcome. */ + CONDITION_ON_MEASUREMENT = 7; +} + +/* Logic operators to be used for determining the outcome of rules with logic operator condition. */ +enum LogicOperator { + + /* Invalid use */ + LOGIC_OPERATOR_UNSPECIFIED = 0; + + /* Logic AND */ + LOGIC_OPERATOR_AND = 1; + + /* Logic OR */ + LOGIC_OPERATOR_OR = 2; +} + +/* Action to be triggered. Currently the only possible action is to trigger the transmission. */ +/* Other actions will be available in next SW releases. */ +enum Action { + + /* Invalid value */ + ACTION_UNSPECIFIED = 0; + + /* To trigger the transmission */ + ACTION_TRIGGER_TRANSMISSION = 1; + + /* To take no action. Possible for logic operator components */ + ACTION_NO_ACTION = 2; + + /* To trigger the transmission with ACK */ + ACTION_TRIGGER_TRANSMISSION_WITH_ACK = 3; +} + +/* Type of a rule calendars. */ +enum CalendarType { + + /* Invalid value */ + CALENDAR_TYPE_UNSPECIFIED = 0; + + /* Type for inactive calendars */ + CALENDAR_TYPE_DISABLED = 1; + + /* Week type. Enables selcted rules on specified days of the week in specified time periods. */ + /* parameter[0] - Week day mask. Bitmask of days when selected rules are enabled */ + /* - Bit 0 - Sunday */ + /* - Bit 1 - Monday */ + /* ... */ + /* - Bit 6 - Saturday */ + /* parameter[1] - 'From time' - point in time from which selected rules will be enabled (in minutes from midnight). */ + /* parameter[2] - 'To time' - point in time from which selected rules will be disabled (in minutes from midnight). */ + /* Note: if 'From time' is bigger than 'To time' there are two periods when rules are enabled - from 00:00 to 'To time' */ + /* and from 'From time' to 23:59. */ + /* parameter[3] - Timezone - desired timezone for date comparison. Encoded as number (N) of 15 minutes offsets */ + /* - example - if N = 4, then offset = 4 * 15min = 1h. I.e. timezone is UTC+1. */ + CALENDAR_TYPE_WEEK = 2; +} + +/* Rules calendars. Used for enabling/disabling rules based on date/time. */ +/* It is possible to configure up to 6 calendars. Each of them can affect any number of rules. */ +message ProtoCalendar { + + /* Bit mask of selected rules. Mask on bits [0:11] */ + /* - Bit 0 - Rule ID 0 */ + /* - Bit 1 - Rule ID 1 */ + /* ... */ + /* - Bit 11 - Rule ID 11 */ + uint32 rule_mask = 1; + + /* Calendars's parameters. Described in Type. */ + repeated sint32 parameters = 2; + + /* Calendar's type. Described in Type. */ + CalendarType type = 3; +} + +/* Rules used to define edge logic on the device. Rules are defined by conditions and actions: */ +/* If Condition is true, trigger Action. It is possible to configure up to 12 rules and assign them to different channels. */ +/* One rule can be assigned to any number of channels. For instance rule "If temperature is over 10 C, trigger the transmission"*/ +/* can be assigned to channels 1 and 2. No matter to how many channels a rule is assigned, it's still counted as one rule. */ +message ProtoRule { + + /* Channels to which the rule is assigned. One rule can be assigned to multiple channels as long as those are of the same type*/ + /* Bit mask on bits [0:5]. E.g. To assign the rule for channel 1: "000001", to assign rule to channels 2 and 4: "001010" */ + uint32 channel_mask = 1; + + /* Rule's condition (as described in Condition). */ + Condition condition = 2; + + /* Condition's parameters (as described in Condition). For binary sensors there are no parameters */ + repeated sint32 parameters = 3; + + /* Action to be triggered. */ + Action action = 4; +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 63b13bca66..9688f7b7e1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -33,6 +33,7 @@ import java.util.Optional; import java.util.UUID; import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.Collectors; public abstract class DaoUtil { @@ -109,6 +110,10 @@ public abstract class DaoUtil { return ids; } + public static List fromUUIDs(List uuids, Function mapper) { + return uuids.stream().map(mapper).collect(Collectors.toList()); + } + public static I toEntityId(UUID uuid, Function creator) { if (uuid != null) { return creator.apply(uuid); 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 745861c6c8..93ebc61581 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 @@ -77,6 +77,8 @@ public interface AlarmDao extends Dao { PageData findAlarmsIdsByEndTsBeforeAndTenantId(Long time, TenantId tenantId, PageLink pageLink); + PageData findAlarmIdsByAssigneeId(TenantId tenantId, UUID userId, PageLink pageLink); + void createEntityAlarmRecord(EntityAlarm entityAlarm); List findEntityAlarmRecords(TenantId tenantId, AlarmId id); 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 85428e14a9..b26daba312 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 @@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.HasId; 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.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -384,6 +385,13 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return Futures.immediateFuture(alarmDao.findCustomerAlarmsV2(tenantId, customerId, query)); } + @Override + public PageData findAlarmIdsByAssigneeId(TenantId tenantId, UserId userId, PageLink pageLink) { + log.trace("[{}] Executing findAlarmIdsByAssigneeId [{}]", tenantId, userId); + validateId(userId, "Incorrect userId " + userId); + return alarmDao.findAlarmIdsByAssigneeId(tenantId, userId.getId(), pageLink); + } + @Override public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) { @@ -522,5 +530,4 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ request.setEndTs(request.getStartTs()); } } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/eventsourcing/DeleteEntityEvent.java b/dao/src/main/java/org/thingsboard/server/dao/eventsourcing/DeleteEntityEvent.java index 6277b7910b..7d0499e8aa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/eventsourcing/DeleteEntityEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/eventsourcing/DeleteEntityEvent.java @@ -28,4 +28,7 @@ public class DeleteEntityEvent { private final EntityId entityId; private final EdgeId edgeId; private final T entity; + + @Builder.Default + private final long ts = System.currentTimeMillis(); } 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 92537dc850..7db3ea24f8 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 @@ -21,6 +21,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.sql.AlarmEntity; import org.thingsboard.server.dao.model.sql.AlarmInfoEntity; @@ -315,6 +316,9 @@ public interface AlarmRepository extends JpaRepository { @Query(value = "SELECT a FROM AlarmInfoEntity a WHERE a.tenantId = :tenantId AND a.id = :alarmId") AlarmInfoEntity findAlarmInfoById(@Param("tenantId") UUID tenantId, @Param("alarmId") UUID alarmId); + @Query("SELECT a.id FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.assigneeId = :assigneeId") + Page findAlarmIdsByAssigneeId(@Param("tenantId") UUID tenantId, @Param("assigneeId") UUID assigneeId, Pageable pageable); + @Query(value = "SELECT create_or_update_active_alarm(:t_id, :c_id, :a_id, :a_created_ts, :a_o_id, :a_o_type, :a_type, :a_severity, " + ":a_start_ts, :a_end_ts, :a_details, :a_propagate, :a_propagate_to_owner, " + ":a_propagate_to_tenant, :a_propagation_types, :a_creation_enabled)", nativeQuery = true) 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 8dfdaf8a3e..b6a1d0c92e 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 @@ -285,6 +285,12 @@ public class JpaAlarmDao extends JpaAbstractDao implements A .mapData(AlarmId::new); } + @Override + public PageData findAlarmIdsByAssigneeId(TenantId tenantId, UUID userId, PageLink pageLink) { + return DaoUtil.pageToPageData(alarmRepository.findAlarmIdsByAssigneeId(tenantId.getId(), userId, DaoUtil.toPageable(pageLink))) + .mapData(AlarmId::new); + } + @Override public void createEntityAlarmRecord(EntityAlarm entityAlarm) { log.debug("Saving entity {}", entityAlarm); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index fcd9612d92..9776e5fe51 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -55,6 +55,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import static org.thingsboard.server.common.data.StringUtils.generateSafeToken; @@ -246,8 +247,10 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @Override @Transactional - public void deleteUser(TenantId tenantId, UserId userId) { - log.trace("Executing deleteUser [{}]", userId); + public void deleteUser(TenantId tenantId, User user) { + Objects.requireNonNull(user, "User is null"); + UserId userId = user.getId(); + log.trace("[{}] Executing deleteUser [{}]", tenantId, userId); validateId(userId, INCORRECT_USER_ID + userId); UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, userId.getId()); userCredentialsDao.removeById(tenantId, userCredentials.getUuidId()); @@ -258,7 +261,8 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic countService.publishCountEntityEvictEvent(tenantId, EntityType.USER); eventPublisher.publishEvent(DeleteEntityEvent.builder() .tenantId(tenantId) - .entityId(userId).build()); + .entityId(userId) + .entity(user).build()); } @Override @@ -443,7 +447,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @Override protected void removeEntity(TenantId tenantId, User entity) { - deleteUser(tenantId, new UserId(entity.getUuidId())); + deleteUser(tenantId, entity); } }; @@ -456,7 +460,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @Override protected void removeEntity(TenantId tenantId, User entity) { - deleteUser(tenantId, new UserId(entity.getUuidId())); + deleteUser(tenantId, entity); } }; diff --git a/dao/src/test/java/org/thingsboard/server/dao/eventsourcing/DeleteEntityEventTest.java b/dao/src/test/java/org/thingsboard/server/dao/eventsourcing/DeleteEntityEventTest.java new file mode 100644 index 0000000000..39d228ceac --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/eventsourcing/DeleteEntityEventTest.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.eventsourcing; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.BDDAssertions.byLessThan; + +class DeleteEntityEventTest { + + @Test + void testBuilderDefaultTs() { + assertThat(DeleteEntityEvent.builder().build().getTs()) + .isCloseTo(System.currentTimeMillis(), byLessThan(TimeUnit.MINUTES.toMillis(1))); + + assertThat(DeleteEntityEvent.builder().ts(Long.MIN_VALUE).build().getTs()) + .isEqualTo(Long.MIN_VALUE); + assertThat(DeleteEntityEvent.builder().ts(Long.MAX_VALUE).build().getTs()) + .isEqualTo(Long.MAX_VALUE); + assertThat(DeleteEntityEvent.builder().ts(-1L).build().getTs()) + .isEqualTo(-1L); + assertThat(DeleteEntityEvent.builder().ts(0L).build().getTs()) + .isEqualTo(0L); + + assertThat(DeleteEntityEvent.builder().ts(1692175215000L).build().getTs()) + .isEqualTo(1692175215000L); + } + +} \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java index 28e4f955dc..711d3ed805 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java @@ -135,7 +135,7 @@ public class UserServiceTest extends AbstractServiceTest { Assert.assertEquals("Joe", savedUser.getFirstName()); Assert.assertEquals("Downs", savedUser.getLastName()); - userService.deleteUser(tenantId, savedUser.getId()); + userService.deleteUser(tenantId, savedUser); } @Test @@ -188,7 +188,7 @@ public class UserServiceTest extends AbstractServiceTest { Assert.assertNotNull(foundUser); UserCredentials userCredentials = userService.findUserCredentialsByUserId(tenantId, foundUser.getId()); Assert.assertNotNull(userCredentials); - userService.deleteUser(tenantId, foundUser.getId()); + userService.deleteUser(tenantId, foundUser); userCredentials = userService.findUserCredentialsByUserId(tenantId, foundUser.getId()); foundUser = userService.findUserById(tenantId, foundUser.getId()); Assert.assertNull(foundUser); @@ -301,7 +301,7 @@ public class UserServiceTest extends AbstractServiceTest { Assert.assertEquals(tenantAdminsEmail2, loadedTenantAdminsEmail2); for (User user : loadedTenantAdminsEmail1) { - userService.deleteUser(tenantId, user.getId()); + userService.deleteUser(tenantId, user); } pageLink = new PageLink(4, 0, email1); @@ -310,7 +310,7 @@ public class UserServiceTest extends AbstractServiceTest { Assert.assertEquals(0, pageData.getData().size()); for (User user : loadedTenantAdminsEmail2) { - userService.deleteUser(tenantId, user.getId()); + userService.deleteUser(tenantId, user); } pageLink = new PageLink(4, 0, email2); @@ -440,7 +440,7 @@ public class UserServiceTest extends AbstractServiceTest { Assert.assertEquals(customerUsersEmail2, loadedCustomerUsersEmail2); for (User user : loadedCustomerUsersEmail1) { - userService.deleteUser(tenantId, user.getId()); + userService.deleteUser(tenantId, user); } pageLink = new PageLink(4, 0, email1); @@ -449,7 +449,7 @@ public class UserServiceTest extends AbstractServiceTest { Assert.assertEquals(0, pageData.getData().size()); for (User user : loadedCustomerUsersEmail2) { - userService.deleteUser(tenantId, user.getId()); + userService.deleteUser(tenantId, user); } pageLink = new PageLink(4, 0, email2);