diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java index 3fdc1bc236..fbb8901ca9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; 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.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.EntityCountQuery; @@ -93,6 +94,20 @@ public class EntityQueryController extends BaseController { return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); } + @ApiOperation(value = "Count Alarms by Query (countAlarmsByQuery)", notes = "Returns the number of alarms that match the query definition.") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarmsQuery/count", method = RequestMethod.POST) + @ResponseBody + public long countAlarmsByQuery(@ApiParam(value = "A JSON value representing the alarm count query.") + @RequestBody AlarmCountQuery query) throws ThingsboardException { + checkNotNull(query); + UserId assigneeId = query.getAssigneeId(); + if (assigneeId != null) { + checkUserId(assigneeId, Operation.READ); + } + return this.entityQueryService.countAlarmsByQuery(getCurrentUser(), query); + } + @ApiOperation(value = "Find Entity Keys by Query", notes = "Uses entity data query (see 'Find Entity Data by Query') to find first 100 entities. Then fetch and return all unique time-series and/or attribute keys. Used mostly for UI hints.") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index 5a9d0d9edf..d0cd58aa90 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; @@ -205,6 +206,11 @@ public class DefaultEntityQueryService implements EntityQueryService { } } + @Override + public long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) { + return alarmService.countAlarmsByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query); + } + private EntityDataQuery buildEntityDataQuery(AlarmDataQuery query) { EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder(); EntityDataSortOrder entitiesSortOrder; diff --git a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java index b03fb02b33..cb936de31c 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java @@ -19,6 +19,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.EntityCountQuery; @@ -34,6 +35,8 @@ public interface EntityQueryService { PageData findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query); + long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query); + DeferredResult getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, boolean isTimeseries, boolean isAttributes); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index 28efbdfee0..4b37ba26b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -54,6 +54,7 @@ import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggHistoryCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggKey; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggTimeSeriesCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; @@ -408,6 +409,26 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } } + @Override + public void handleCmd(WebSocketSessionRef session, AlarmCountCmd cmd) { + TbAlarmCountSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId()); + if (ctx == null) { + ctx = createSubCtx(session, cmd); + long start = System.currentTimeMillis(); + ctx.fetchData(); + long end = System.currentTimeMillis(); + stats.getAlarmQueryInvocationCnt().incrementAndGet(); + stats.getAlarmQueryTimeSpent().addAndGet(end - start); + TbAlarmCountSubCtx finalCtx = ctx; + ScheduledFuture task = scheduler.scheduleWithFixedDelay( + () -> refreshDynamicQuery(finalCtx), + dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS); + finalCtx.setRefreshTask(task); + } else { + log.debug("[{}][{}] Received duplicate command: {}", session.getSessionId(), cmd.getCmdId(), cmd); + } + } + private boolean validate(TbAbstractSubCtx finalCtx) { if (finalCtx.isStopped()) { log.warn("[{}][{}][{}] Received validation task for already stopped context.", finalCtx.getTenantId(), finalCtx.getSessionId(), finalCtx.getCmdId()); @@ -501,6 +522,17 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc return ctx; } + private TbAlarmCountSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) { + Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); + TbAlarmCountSubCtx ctx = new TbAlarmCountSubCtx(serviceId, wsService, entityService, localSubscriptionService, + attributesService, stats, alarmService, sessionRef, cmd.getCmdId()); + if (cmd.getQuery() != null) { + ctx.setAndResolveQuery(cmd.getQuery()); + } + sessionSubs.put(cmd.getCmdId(), ctx); + return ctx; + } + @SuppressWarnings("unchecked") private T getSubCtx(String sessionId, int cmdId) { Map sessionSubs = subscriptionsBySessionId.get(sessionId); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java new file mode 100644 index 0000000000..bb7b9350fb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java @@ -0,0 +1,67 @@ +/** + * 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.subscription; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.query.AlarmCountQuery; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.service.ws.WebSocketService; +import org.thingsboard.server.service.ws.WebSocketSessionRef; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate; + +@Slf4j +@ToString(callSuper = true) +public class TbAlarmCountSubCtx extends TbAbstractSubCtx { + + private final AlarmService alarmService; + + @Getter + @Setter + private volatile int result; + + public TbAlarmCountSubCtx(String serviceId, WebSocketService wsService, + EntityService entityService, TbLocalSubscriptionService localSubscriptionService, + AttributesService attributesService, SubscriptionServiceStatistics stats, AlarmService alarmService, + WebSocketSessionRef sessionRef, int cmdId) { + super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId); + this.alarmService = alarmService; + } + + @Override + public void fetchData() { + result = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query); + sendWsMsg(new AlarmCountUpdate(cmdId, result)); + } + + @Override + protected void update() { + int newCount = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query); + if (newCount != result) { + result = newCount; + sendWsMsg(new AlarmCountUpdate(cmdId, result)); + } + } + + @Override + public boolean isDynamic() { + return true; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java index cd6921282e..097fa792f9 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.subscription; import org.thingsboard.server.service.ws.WebSocketSessionRef; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; @@ -29,6 +30,8 @@ public interface TbEntityDataSubscriptionService { void handleCmd(WebSocketSessionRef sessionId, AlarmDataCmd cmd); + void handleCmd(WebSocketSessionRef sessionId, AlarmCountCmd cmd); + void cancelSubscription(String sessionId, UnsubscribeCmd subscriptionId); void cancelAllSessionSubscriptions(String sessionId); diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index 258af9d2e4..21d69d6b42 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -72,6 +72,7 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v1.GetHistoryCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v1.SubscriptionCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v1.TelemetryPluginCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v1.TimeseriesSubscriptionCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; @@ -166,10 +167,11 @@ public class DefaultWebSocketService implements WebSocketService { newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityDataCmds, this::handleWsEntityDataCmd), newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmDataCmds, this::handleWsAlarmDataCmd), newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityCountCmds, this::handleWsEntityCountCmd), + newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmCountCmds, this::handleWsAlarmCountCmd), newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityDataUnsubscribeCmds, this::handleWsDataUnsubscribeCmd), newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmDataUnsubscribeCmds, this::handleWsDataUnsubscribeCmd), - newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmDataUnsubscribeCmds, this::handleWsDataUnsubscribeCmd), - newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityCountUnsubscribeCmds, this::handleWsDataUnsubscribeCmd) + newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityCountUnsubscribeCmds, this::handleWsDataUnsubscribeCmd), + newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmCountUnsubscribeCmds, this::handleWsDataUnsubscribeCmd) ); notificationCmdsHandlers = List.of( newCmdHandler(NotificationCmdsWrapper::getUnreadSubCmd, notificationCmdsHandler::handleUnreadNotificationsSubCmd), @@ -302,6 +304,16 @@ public class DefaultWebSocketService implements WebSocketService { } } + private void handleWsAlarmCountCmd(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) { + String sessionId = sessionRef.getSessionId(); + log.debug("[{}] Processing: {}", sessionId, cmd); + + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId) + && validateSubscriptionCmd(sessionRef, cmd)) { + entityDataSubService.handleCmd(sessionRef, cmd); + } + } + @Override public void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update) { sendWsMsg(sessionId, update.getSubscriptionId(), update); @@ -866,6 +878,16 @@ public class DefaultWebSocketService implements WebSocketService { } } + private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) { + if (cmd.getCmdId() < 0) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Cmd id is negative value!"); + sendWsMsg(sessionRef, update); + return false; + } + return true; + } + private void sendWsMsg(WebSocketSessionRef sessionRef, EntityDataUpdate update) { sendWsMsg(sessionRef, update.getCmdId(), update); } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/TelemetryPluginCmdsWrapper.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/TelemetryPluginCmdsWrapper.java index bea4772085..4daf2a939b 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/TelemetryPluginCmdsWrapper.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/TelemetryPluginCmdsWrapper.java @@ -19,6 +19,8 @@ import lombok.Data; import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v1.GetHistoryCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v1.TimeseriesSubscriptionCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUnsubscribeCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUnsubscribeCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUnsubscribeCmd; @@ -52,4 +54,8 @@ public class TelemetryPluginCmdsWrapper { private List entityCountUnsubscribeCmds; + private List alarmCountCmds; + + private List alarmCountUnsubscribeCmds; + } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountCmd.java new file mode 100644 index 0000000000..80fd998eb1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountCmd.java @@ -0,0 +1,34 @@ +/** + * 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.ws.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import org.thingsboard.server.common.data.query.AlarmCountQuery; + +public class AlarmCountCmd extends DataCmd { + + @Getter + private final AlarmCountQuery query; + + @JsonCreator + public AlarmCountCmd(@JsonProperty("cmdId") int cmdId, + @JsonProperty("query") AlarmCountQuery query) { + super(cmdId); + this.query = query; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountUnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountUnsubscribeCmd.java new file mode 100644 index 0000000000..79c860f3f6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountUnsubscribeCmd.java @@ -0,0 +1,25 @@ +/** + * 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.ws.telemetry.cmd.v2; + +import lombok.Data; + +@Data +public class AlarmCountUnsubscribeCmd implements UnsubscribeCmd { + + private final int cmdId; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountUpdate.java new file mode 100644 index 0000000000..b6f70811b8 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountUpdate.java @@ -0,0 +1,53 @@ +/** + * 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.ws.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; + +@ToString +public class AlarmCountUpdate extends CmdUpdate { + + @Getter + private int count; + + public AlarmCountUpdate(int cmdId, int count) { + super(cmdId, SubscriptionErrorCode.NO_ERROR.getCode(), null); + this.count = count; + } + + public AlarmCountUpdate(int cmdId, int errorCode, String errorMsg) { + super(cmdId, errorCode, errorMsg); + } + + @Override + public CmdUpdateType getCmdUpdateType() { + return CmdUpdateType.ALARM_COUNT_DATA; + } + + @JsonCreator + public AlarmCountUpdate(@JsonProperty("cmdId") int cmdId, + @JsonProperty("count") int count, + @JsonProperty("errorCode") int errorCode, + @JsonProperty("errorMsg") String errorMsg) { + super(cmdId, errorCode, errorMsg); + this.count = count; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdateType.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdateType.java index e1a9b32895..f5b3809ce2 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdateType.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdateType.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.ws.telemetry.cmd.v2; public enum CmdUpdateType { ENTITY_DATA, ALARM_DATA, + ALARM_COUNT_DATA, COUNT_DATA, NOTIFICATIONS, NOTIFICATIONS_COUNT diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java index f29827fbe2..c468bb659a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java @@ -29,9 +29,12 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValueSourceType; @@ -52,6 +55,7 @@ import org.thingsboard.server.common.data.security.Authority; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -189,6 +193,137 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe Assert.assertEquals(97, count2.longValue()); } + @Test + public void testTenantCountAlarmsByQuery() throws Exception { + loginTenantAdmin(); + List devices = new ArrayList<>(); + List alarms = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + Device device = new Device(); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(doPost("/api/device", device, Device.class)); + Thread.sleep(1); + } + + for (int i = 0; i < devices.size(); i++) { + Alarm alarm = new Alarm(); + alarm.setOriginator(devices.get(i).getId()); + alarm.setType("alarm" + i); + alarm.setSeverity(AlarmSeverity.WARNING); + alarms.add(doPost("/api/alarm", alarm, Alarm.class)); + Thread.sleep(1); + } + testCountAlarmsByQuery(alarms); + } + + @Test + public void testCustomerCountAlarmsByQuery() throws Exception { + loginTenantAdmin(); + List devices = new ArrayList<>(); + List alarms = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + Device device = new Device(); + device.setCustomerId(customerId); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(doPost("/api/device", device, Device.class)); + Thread.sleep(1); + } + + loginCustomerUser(); + + for (int i = 0; i < devices.size(); i++) { + Alarm alarm = new Alarm(); + alarm.setCustomerId(customerId); + alarm.setOriginator(devices.get(i).getId()); + alarm.setType("alarm" + i); + alarm.setSeverity(AlarmSeverity.WARNING); + alarms.add(doPost("/api/alarm", alarm, Alarm.class)); + Thread.sleep(1); + } + testCountAlarmsByQuery(alarms); + } + + private void testCountAlarmsByQuery(List alarms) throws Exception { + AlarmCountQuery countQuery = new AlarmCountQuery(); + + Long count = doPostWithResponse("/api/alarmsQuery/count", countQuery, Long.class); + Assert.assertEquals(97, count.longValue()); + + countQuery = AlarmCountQuery.builder() + .typeList(List.of("unknown")) + .build(); + + count = doPostWithResponse("/api/alarmsQuery/count", countQuery, Long.class); + Assert.assertEquals(0, count.longValue()); + + countQuery = AlarmCountQuery.builder() + .typeList(List.of("alarm1", "alarm2", "alarm3")) + .build(); + + count = doPostWithResponse("/api/alarmsQuery/count", countQuery, Long.class); + Assert.assertEquals(3, count.longValue()); + + countQuery = AlarmCountQuery.builder() + .typeList(alarms.stream().map(Alarm::getType).collect(Collectors.toList())) + .build(); + + count = doPostWithResponse("/api/alarmsQuery/count", countQuery, Long.class); + Assert.assertEquals(97, count.longValue()); + + countQuery = AlarmCountQuery.builder() + .severityList(List.of(AlarmSeverity.CRITICAL)) + .build(); + + count = doPostWithResponse("/api/alarmsQuery/count", countQuery, Long.class); + Assert.assertEquals(0, count.longValue()); + + countQuery = AlarmCountQuery.builder() + .severityList(List.of(AlarmSeverity.WARNING)) + .build(); + + count = doPostWithResponse("/api/alarmsQuery/count", countQuery, Long.class); + Assert.assertEquals(97, count.longValue()); + + long startTs = alarms.stream().map(Alarm::getCreatedTime).min(Long::compareTo).get(); + long endTs = alarms.stream().map(Alarm::getCreatedTime).max(Long::compareTo).get(); + + countQuery = AlarmCountQuery.builder() + .startTs(startTs - 1) + .endTs(endTs + 1) + .build(); + + count = doPostWithResponse("/api/alarmsQuery/count", countQuery, Long.class); + Assert.assertEquals(97, count.longValue()); + + countQuery = AlarmCountQuery.builder() + .startTs(0) + .endTs(endTs + 1) + .build(); + + count = doPostWithResponse("/api/alarmsQuery/count", countQuery, Long.class); + Assert.assertEquals(97, count.longValue()); + + countQuery = AlarmCountQuery.builder() + .startTs(0) + .endTs(System.currentTimeMillis()) + .build(); + + count = doPostWithResponse("/api/alarmsQuery/count", countQuery, Long.class); + Assert.assertEquals(97, count.longValue()); + + countQuery = AlarmCountQuery.builder() + .startTs(endTs + 1) + .endTs(System.currentTimeMillis()) + .build(); + + count = doPostWithResponse("/api/alarmsQuery/count", countQuery, Long.class); + Assert.assertEquals(0, count.longValue()); + } + @Test public void testSimpleFindEntityDataByQuery() throws Exception { List devices = new ArrayList<>(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java index f5904ecb82..b4742bdd77 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java @@ -330,7 +330,6 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest { Assert.assertTrue(featuresInfo.isOauthEnabled()); } - private OAuth2Info createDefaultOAuth2Info() { return new OAuth2Info(true, Lists.newArrayList( OAuth2ParamsInfo.builder() diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java index d26f6b190d..a452cbdce4 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java @@ -26,6 +26,8 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -35,6 +37,7 @@ import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; @@ -51,6 +54,8 @@ import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.service.subscription.SubscriptionErrorCode; import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; @@ -237,6 +242,74 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { Assert.assertEquals(0, update4.getCount()); } + @Test + public void testAlarmCountWsCmd() throws Exception { + loginTenantAdmin(); + + AlarmCountCmd cmd1 = new AlarmCountCmd(1, new AlarmCountQuery()); + + getWsClient().send(cmd1); + + AlarmCountUpdate update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); + Assert.assertEquals(1, update.getCmdId()); + Assert.assertEquals(0, update.getCount()); + + Alarm alarm = new Alarm(); + alarm.setOriginator(tenantId); + alarm.setType("TEST ALARM"); + alarm.setSeverity(AlarmSeverity.WARNING); + + alarm = doPost("/api/alarm", alarm, Alarm.class); + + AlarmCountCmd cmd2 = new AlarmCountCmd(2, new AlarmCountQuery()); + + getWsClient().send(cmd2); + + update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); + Assert.assertEquals(2, update.getCmdId()); + Assert.assertEquals(1, update.getCount()); + + AlarmCountCmd cmd3 = new AlarmCountCmd(3, AlarmCountQuery.builder().assigneeId(tenantAdminUserId).build()); + + getWsClient().send(cmd3); + + update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); + Assert.assertEquals(3, update.getCmdId()); + Assert.assertEquals(0, update.getCount()); + + alarm.setAssigneeId(tenantAdminUserId); + alarm = doPost("/api/alarm", alarm, Alarm.class); + + AlarmCountCmd cmd4 = new AlarmCountCmd(4, AlarmCountQuery.builder().assigneeId(tenantAdminUserId).build()); + + getWsClient().send(cmd4); + + update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); + Assert.assertEquals(4, update.getCmdId()); + Assert.assertEquals(1, update.getCount()); + + AlarmCountCmd cmd5 = new AlarmCountCmd(5, + AlarmCountQuery.builder().severityList(Collections.singletonList(AlarmSeverity.CRITICAL)).build()); + + getWsClient().send(cmd5); + + update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); + Assert.assertEquals(5, update.getCmdId()); + Assert.assertEquals(0, update.getCount()); + + alarm.setSeverity(AlarmSeverity.CRITICAL); + doPost("/api/alarm", alarm, Alarm.class); + + AlarmCountCmd cmd6 = new AlarmCountCmd(6, + AlarmCountQuery.builder().severityList(Collections.singletonList(AlarmSeverity.CRITICAL)).build()); + + getWsClient().send(cmd6); + + update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply()); + Assert.assertEquals(6, update.getCmdId()); + Assert.assertEquals(1, update.getCount()); + } + @Test public void testEntityDataLatestWidgetFlow() throws Exception { List keys = List.of(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index 2366a631fa..5d66342dca 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -30,6 +30,8 @@ import org.thingsboard.server.common.data.query.EntityFilter; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryPluginCmdsWrapper; import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; @@ -115,6 +117,12 @@ public class TbTestWebSocketClient extends WebSocketClient { this.send(JacksonUtil.toString(wrapper)); } + public void send(AlarmCountCmd cmd) throws NotYetConnectedException { + TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper(); + wrapper.setAlarmCountCmds(Collections.singletonList(cmd)); + this.send(JacksonUtil.toString(wrapper)); + } + public String waitForUpdate() { return waitForUpdate(false); } @@ -179,6 +187,10 @@ public class TbTestWebSocketClient extends WebSocketClient { return JacksonUtil.fromString(msg, EntityCountUpdate.class); } + public AlarmCountUpdate parseAlarmCountReply(String msg) { + return JacksonUtil.fromString(msg, AlarmCountUpdate.class); + } + public EntityDataUpdate subscribeLatestUpdate(List keys, EntityFilter entityFilter) { EntityDataQuery edq = new EntityDataQuery(entityFilter, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); 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 2b655562be..75fa20fd57 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 @@ -19,19 +19,20 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; -import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.entity.EntityDaoService; @@ -113,4 +114,6 @@ public interface AlarmService extends EntityDaoService { AlarmDataQuery query, Collection orderedEntityIds); void deleteEntityAlarmRelations(TenantId tenantId, EntityId entityId); + + long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java new file mode 100644 index 0000000000..5902e9f52f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java @@ -0,0 +1,43 @@ +/** + * 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.common.data.query; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.id.UserId; + +import java.util.List; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@ToString +public class AlarmCountQuery extends EntityCountQuery { + private long startTs; + private long endTs; + private long timeWindow; + private List typeList; + private List statusList; + private List severityList; + private boolean searchPropagatedAlarms; + private UserId assigneeId; +} 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 37e387a9e9..90134f0778 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 @@ -19,12 +19,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; -import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; @@ -33,6 +33,7 @@ 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; import org.thingsboard.server.dao.Dao; @@ -89,4 +90,6 @@ public interface AlarmDao extends Dao { AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long unassignTime); + long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); + } 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 767f49ba83..dbdce7718b 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 @@ -28,15 +28,15 @@ import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; -import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; -import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.exception.ApiUsageLimitsExceededException; import org.thingsboard.server.common.data.id.AlarmId; @@ -46,6 +46,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.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -358,6 +359,12 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ alarmDao.deleteEntityAlarmRecords(tenantId, entityId); } + @Override + public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) { + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return alarmDao.countAlarmsByQuery(tenantId, customerId, query); + } + private Alarm merge(Alarm existing, Alarm alarm) { if (alarm.getStartTs() > existing.getEndTs()) { existing.setEndTs(alarm.getStartTs()); 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 2c0b52f781..e8eb0b150c 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 @@ -29,13 +29,13 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmAssignee; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; -import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; @@ -45,6 +45,7 @@ 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; import org.thingsboard.server.dao.DaoUtil; @@ -297,6 +298,11 @@ public class JpaAlarmDao extends JpaAbstractDao implements A return toAlarmApiResult(alarmRepository.unassignAlarm(tenantId.getId(), id.getId(), unassignTime)); } + @Override + public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) { + return alarmQueryRepository.countAlarmsByQuery(tenantId, customerId, query); + } + @NotNull private static String getPropagationTypes(AlarmPropagationInfo ap) { String propagateRelationTypes; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java index 366e57f0cf..dd65eb88d8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.dao.sql.query; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -28,4 +30,6 @@ public interface AlarmQueryRepository { PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); + long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index 309b151a72..85e3917ceb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -19,13 +19,16 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Repository; import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataPageLink; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -305,6 +308,95 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { }); } + @Override + public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) { + QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, null, EntityType.ALARM)); + + ctx.append("select count(id) from alarm_info a "); + + if (query.isSearchPropagatedAlarms()) { + ctx.append(JOIN_ENTITY_ALARMS); + ctx.append("where a.tenant_id = :tenantId and ea.tenant_id = :tenantId"); + ctx.addUuidParameter("tenantId", tenantId.getId()); + if (customerId != null && !customerId.isNullUid()) { + ctx.append(" and a.customer_id = :customerId and ea.customer_id = :customerId"); + ctx.addUuidParameter("customerId", customerId.getId()); + } + } else { + ctx.append("where a.tenant_id = :tenantId"); + ctx.addUuidParameter("tenantId", tenantId.getId()); + if (customerId != null && !customerId.isNullUid()) { + ctx.append(" and a.customer_id = :customerId"); + ctx.addUuidParameter("customerId", customerId.getId()); + } + } + + long startTs; + long endTs; + if (query.getTimeWindow() > 0) { + endTs = System.currentTimeMillis(); + startTs = endTs - query.getTimeWindow(); + } else { + startTs = query.getStartTs(); + endTs = query.getEndTs(); + } + + if (startTs > 0) { + ctx.append(" and a.created_time >= :startTime"); + ctx.addLongParameter("startTime", startTs); + if (query.isSearchPropagatedAlarms()) { + ctx.append(" and ea.created_time >= :startTime"); + } + } + + if (endTs > 0) { + ctx.append(" and a.created_time <= :endTime"); + ctx.addLongParameter("endTime", endTs); + if (query.isSearchPropagatedAlarms()) { + ctx.append(" and ea.created_time <= :endTime"); + } + } + + if (!CollectionUtils.isEmpty(query.getTypeList())) { + ctx.append(" and a.type in (:alarmTypes)"); + ctx.addStringListParameter("alarmTypes", query.getTypeList()); + if (query.isSearchPropagatedAlarms()) { + ctx.append(" and ea.alarm_type in (:alarmTypes)"); + } + } + + if (query.getSeverityList() != null && !query.getSeverityList().isEmpty()) { + ctx.append(" and a.severity in (:alarmSeverities)"); + ctx.addStringListParameter("alarmSeverities", query.getSeverityList().stream().map(AlarmSeverity::name).collect(Collectors.toList())); + } + + AlarmStatusFilter asf = AlarmStatusFilter.from(query.getStatusList()); + if (asf.hasAnyFilter()) { + if (asf.hasAckFilter()) { + ctx.append(" and a.acknowledged = :ackStatus"); + ctx.addBooleanParameter("ackStatus", asf.getAckFilter()); + } + if (asf.hasClearFilter()) { + ctx.append(" and a.cleared = :clearStatus"); + ctx.addBooleanParameter("clearStatus", asf.getClearFilter()); + } + } + + if (query.getAssigneeId() != null) { + ctx.addUuidParameter("assigneeId", query.getAssigneeId().getId()); + ctx.append(" and a.assignee_id = :assigneeId"); + } + + return transactionTemplate.execute(trStatus -> { + long queryTs = System.currentTimeMillis(); + try { + return jdbcTemplate.queryForObject(ctx.getQuery(), ctx, Long.class); + } finally { + queryLog.logQuery(ctx, ctx.getQuery(), System.currentTimeMillis() - queryTs); + } + }); + } + private String buildTextSearchQuery(QueryContext ctx, List selectionMapping, String searchText) { if (!StringUtils.isEmpty(searchText) && selectionMapping != null && !selectionMapping.isEmpty()) { String lowerSearchText = searchText.toLowerCase() + "%"; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java index 352e3b57fa..f051ac2963 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataPageLink; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -699,6 +700,69 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); } + @Test + public void testCountAlarmsUsingAlarmDataQuery() throws ExecutionException, InterruptedException { + AssetId childId = new AssetId(Uuids.timeBased()); + + long ts = System.currentTimeMillis(); + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(childId) + .type(TEST_ALARM) + .severity(AlarmSeverity.CRITICAL) + .startTs(ts).build()); + AlarmInfo created = result.getAlarm(); + + AlarmCountQuery countQuery = AlarmCountQuery.builder() + .startTs(0L) + .endTs(System.currentTimeMillis()) + .searchPropagatedAlarms(false) + .severityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)) + .statusList(List.of(AlarmSearchStatus.ACTIVE)) + .build(); + + long alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery); + + Assert.assertEquals(1, alarmsCount); + + countQuery = AlarmCountQuery.builder() + .startTs(0L) + .endTs(System.currentTimeMillis()) + .searchPropagatedAlarms(true) + .severityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)) + .statusList(List.of(AlarmSearchStatus.ACTIVE)) + .build(); + + alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery); + + Assert.assertEquals(1, alarmsCount); + + created = alarmService.acknowledgeAlarm(tenantId, created.getId(), System.currentTimeMillis()).getAlarm(); + + alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery); + + Assert.assertEquals(1, alarmsCount); + + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + created = alarmService.findAlarmInfoById(tenantId, created.getId()); + + alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery); + + Assert.assertEquals(0, alarmsCount); + + countQuery = AlarmCountQuery.builder() + .startTs(0L) + .endTs(System.currentTimeMillis()) + .searchPropagatedAlarms(true) + .severityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)) + .statusList(List.of(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.CLEARED)) + .build(); + + alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery); + + Assert.assertEquals(1, alarmsCount); + } + @Test public void testDeleteAlarm() throws ExecutionException, InterruptedException { AssetId parentId = new AssetId(Uuids.timeBased()); diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index e84113524a..f0a002714a 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -122,6 +122,7 @@ import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.EntityCountQuery; @@ -1649,6 +1650,10 @@ public class RestClient implements Closeable { }).getBody(); } + public Long countAlarmsByQuery(AlarmCountQuery query) { + return restTemplate.postForObject(baseURL + "/api/alarmsQuery/count", query, Long.class); + } + public void saveRelation(EntityRelation relation) { restTemplate.postForLocation(baseURL + "/api/relation", relation); }