Commit bccbecadf4d8473ef2fc5b33c706877a73824f08

Authored by Vladyslav_Prykhodko
2 parents 886d6167 228fddb8

Merge remote-tracking branch 'upstream/master' into improvement/device-credential/select-type

Showing 35 changed files with 1250 additions and 173 deletions
... ... @@ -135,7 +135,7 @@ public class AuthController extends BaseController {
135 135 }
136 136 }
137 137
138   - @RequestMapping(value = "/noauth/activate", params = { "activateToken" }, method = RequestMethod.GET)
  138 + @RequestMapping(value = "/noauth/activate", params = {"activateToken"}, method = RequestMethod.GET)
139 139 public ResponseEntity<String> checkActivateToken(
140 140 @RequestParam(value = "activateToken") String activateToken) {
141 141 HttpHeaders headers = new HttpHeaders();
... ... @@ -159,7 +159,7 @@ public class AuthController extends BaseController {
159 159
160 160 @RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST)
161 161 @ResponseStatus(value = HttpStatus.OK)
162   - public void requestResetPasswordByEmail (
  162 + public void requestResetPasswordByEmail(
163 163 @RequestBody JsonNode resetPasswordByEmailRequest,
164 164 HttpServletRequest request) throws ThingsboardException {
165 165 try {
... ... @@ -170,13 +170,13 @@ public class AuthController extends BaseController {
170 170 String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
171 171 userCredentials.getResetToken());
172 172
173   - mailService.sendResetPasswordEmail(resetUrl, email);
  173 + mailService.sendResetPasswordEmailAsync(resetUrl, email);
174 174 } catch (Exception e) {
175   - throw handleException(e);
  175 + log.warn("Error occurred: {}", e.getMessage());
176 176 }
177 177 }
178 178
179   - @RequestMapping(value = "/noauth/resetPassword", params = { "resetToken" }, method = RequestMethod.GET)
  179 + @RequestMapping(value = "/noauth/resetPassword", params = {"resetToken"}, method = RequestMethod.GET)
180 180 public ResponseEntity<String> checkResetToken(
181 181 @RequestParam(value = "resetToken") String resetToken) {
182 182 HttpHeaders headers = new HttpHeaders();
... ...
... ... @@ -25,6 +25,7 @@ import org.springframework.security.authentication.BadCredentialsException;
25 25 import org.springframework.security.authentication.DisabledException;
26 26 import org.springframework.security.authentication.LockedException;
27 27 import org.springframework.security.core.AuthenticationException;
  28 +import org.springframework.security.core.userdetails.UsernameNotFoundException;
28 29 import org.springframework.security.web.access.AccessDeniedHandler;
29 30 import org.springframework.web.bind.annotation.ExceptionHandler;
30 31 import org.springframework.web.bind.annotation.RestControllerAdvice;
... ... @@ -152,7 +153,7 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
152 153
153 154 private void handleAuthenticationException(AuthenticationException authenticationException, HttpServletResponse response) throws IOException {
154 155 response.setStatus(HttpStatus.UNAUTHORIZED.value());
155   - if (authenticationException instanceof BadCredentialsException) {
  156 + if (authenticationException instanceof BadCredentialsException || authenticationException instanceof UsernameNotFoundException) {
156 157 mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
157 158 } else if (authenticationException instanceof DisabledException) {
158 159 mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
... ...
... ... @@ -15,6 +15,7 @@
15 15 */
16 16 package org.thingsboard.server.service.install.update;
17 17
  18 +import com.fasterxml.jackson.databind.JsonNode;
18 19 import com.fasterxml.jackson.databind.node.ObjectNode;
19 20 import com.google.common.util.concurrent.Futures;
20 21 import com.google.common.util.concurrent.ListenableFuture;
... ... @@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.Tenant;
31 32 import org.thingsboard.server.common.data.alarm.Alarm;
32 33 import org.thingsboard.server.common.data.alarm.AlarmInfo;
33 34 import org.thingsboard.server.common.data.alarm.AlarmQuery;
  35 +import org.thingsboard.server.common.data.alarm.AlarmSeverity;
34 36 import org.thingsboard.server.common.data.id.EntityViewId;
35 37 import org.thingsboard.server.common.data.id.TenantId;
36 38 import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
... ... @@ -41,16 +43,21 @@ import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientsParams;
41 43 import org.thingsboard.server.common.data.page.PageData;
42 44 import org.thingsboard.server.common.data.page.PageLink;
43 45 import org.thingsboard.server.common.data.page.TimePageLink;
  46 +import org.thingsboard.server.common.data.query.DynamicValue;
  47 +import org.thingsboard.server.common.data.query.FilterPredicateValue;
44 48 import org.thingsboard.server.common.data.rule.RuleChain;
45 49 import org.thingsboard.server.common.data.rule.RuleChainMetaData;
46 50 import org.thingsboard.server.common.data.rule.RuleNode;
  51 +import org.thingsboard.server.dao.DaoUtil;
47 52 import org.thingsboard.server.dao.alarm.AlarmDao;
48 53 import org.thingsboard.server.dao.alarm.AlarmService;
49 54 import org.thingsboard.server.dao.entity.EntityService;
50 55 import org.thingsboard.server.dao.entityview.EntityViewService;
  56 +import org.thingsboard.server.dao.model.sql.DeviceProfileEntity;
51 57 import org.thingsboard.server.dao.oauth2.OAuth2Service;
52 58 import org.thingsboard.server.dao.oauth2.OAuth2Utils;
53 59 import org.thingsboard.server.dao.rule.RuleChainService;
  60 +import org.thingsboard.server.dao.sql.device.DeviceProfileRepository;
54 61 import org.thingsboard.server.dao.tenant.TenantService;
55 62 import org.thingsboard.server.dao.timeseries.TimeseriesService;
56 63 import org.thingsboard.server.service.install.InstallScripts;
... ... @@ -93,6 +100,9 @@ public class DefaultDataUpdateService implements DataUpdateService {
93 100 private AlarmDao alarmDao;
94 101
95 102 @Autowired
  103 + private DeviceProfileRepository deviceProfileRepository;
  104 +
  105 + @Autowired
96 106 private OAuth2Service oAuth2Service;
97 107
98 108 @Override
... ... @@ -114,6 +124,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
114 124 log.info("Updating data from version 3.2.2 to 3.3.0 ...");
115 125 tenantsDefaultEdgeRuleChainUpdater.updateEntities(null);
116 126 tenantsAlarmsCustomerUpdater.updateEntities(null);
  127 + deviceProfileEntityDynamicConditionsUpdater.updateEntities(null);
117 128 updateOAuth2Params();
118 129 break;
119 130 default:
... ... @@ -121,6 +132,45 @@ public class DefaultDataUpdateService implements DataUpdateService {
121 132 }
122 133 }
123 134
  135 + private final PaginatedUpdater<String, DeviceProfileEntity> deviceProfileEntityDynamicConditionsUpdater =
  136 + new PaginatedUpdater<>() {
  137 +
  138 + @Override
  139 + protected String getName() {
  140 + return "Device Profile Entity Dynamic Conditions Updater";
  141 + }
  142 +
  143 + @Override
  144 + protected PageData<DeviceProfileEntity> findEntities(String id, PageLink pageLink) {
  145 + return DaoUtil.pageToPageData(deviceProfileRepository.findAll(DaoUtil.toPageable(pageLink)));
  146 + }
  147 +
  148 + @Override
  149 + protected void updateEntity(DeviceProfileEntity deviceProfile) {
  150 + if (deviceProfile.getProfileData().has("alarms") &&
  151 + !deviceProfile.getProfileData().get("alarms").isNull()) {
  152 + boolean isUpdated = false;
  153 + JsonNode array = deviceProfile.getProfileData().get("alarms");
  154 + for (JsonNode node : array) {
  155 + if (node.has("createRules")) {
  156 + JsonNode createRules = node.get("createRules");
  157 + for (AlarmSeverity severity : AlarmSeverity.values()) {
  158 + if (createRules.has(severity.name())) {
  159 + isUpdated = isUpdated || convertDeviceProfileAlarmRulesForVersion330(createRules.get(severity.name()).get("condition").get("spec"));
  160 + }
  161 + }
  162 + }
  163 + if (node.has("clearRule") && !node.get("clearRule").isNull()) {
  164 + isUpdated = isUpdated || convertDeviceProfileAlarmRulesForVersion330(node.get("clearRule").get("condition").get("spec"));
  165 + }
  166 + }
  167 + if (isUpdated) {
  168 + deviceProfileRepository.save(deviceProfile);
  169 + }
  170 + }
  171 + }
  172 + };
  173 +
124 174 private final PaginatedUpdater<String, Tenant> tenantsDefaultRuleChainUpdater =
125 175 new PaginatedUpdater<>() {
126 176
... ... @@ -370,6 +420,33 @@ public class DefaultDataUpdateService implements DataUpdateService {
370 420 }
371 421 }
372 422
  423 + private boolean convertDeviceProfileAlarmRulesForVersion330(JsonNode spec) {
  424 + if (spec != null) {
  425 + if (spec.has("type") && spec.get("type").asText().equals("DURATION")) {
  426 + if (spec.has("value")) {
  427 + long value = spec.get("value").asLong();
  428 + var predicate = new FilterPredicateValue<>(
  429 + value, null, new DynamicValue<>(null, null, false)
  430 + );
  431 + ((ObjectNode) spec).remove("value");
  432 + ((ObjectNode) spec).putPOJO("predicate", predicate);
  433 + return true;
  434 + }
  435 + } else if (spec.has("type") && spec.get("type").asText().equals("REPEATING")) {
  436 + if (spec.has("count")) {
  437 + int count = spec.get("count").asInt();
  438 + var predicate = new FilterPredicateValue<>(
  439 + count, null, new DynamicValue<>(null, null, false)
  440 + );
  441 + ((ObjectNode) spec).remove("count");
  442 + ((ObjectNode) spec).putPOJO("predicate", predicate);
  443 + return true;
  444 + }
  445 + }
  446 + }
  447 + return false;
  448 + }
  449 +
373 450 private void updateOAuth2Params() {
374 451 try {
375 452 OAuth2ClientsParams oauth2ClientsParams = oAuth2Service.findOAuth2Params();
... ... @@ -380,9 +457,8 @@ public class DefaultDataUpdateService implements DataUpdateService {
380 457 oAuth2Service.saveOAuth2Params(new OAuth2ClientsParams(false, Collections.emptyList()));
381 458 log.info("Successfully updated OAuth2 parameters!");
382 459 }
383   - }
384   - catch (Exception e) {
385   - log.error("Failed to update OAuth2 parameters", e);
  460 + } catch (Exception e) {
  461 + log.error("Failed to update OAuth2 parameters", e);
386 462 }
387 463 }
388 464
... ...
... ... @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.page.PageData;
22 22 import org.thingsboard.server.common.data.page.PageLink;
23 23
24 24 @Slf4j
25   -public abstract class PaginatedUpdater<I, D extends SearchTextBased<? extends UUIDBased>> {
  25 +public abstract class PaginatedUpdater<I, D> {
26 26
27 27 private static final int DEFAULT_LIMIT = 100;
28 28 private int updated = 0;
... ...
... ... @@ -73,6 +73,9 @@ public class DefaultMailService implements MailService {
73 73 @Autowired
74 74 private TbApiUsageStateService apiUsageStateService;
75 75
  76 + @Autowired
  77 + private MailExecutorService mailExecutorService;
  78 +
76 79 private JavaMailSenderImpl mailSender;
77 80
78 81 private String mailFrom;
... ... @@ -222,6 +225,17 @@ public class DefaultMailService implements MailService {
222 225 }
223 226
224 227 @Override
  228 + public void sendResetPasswordEmailAsync(String passwordResetLink, String email) {
  229 + mailExecutorService.execute(() -> {
  230 + try {
  231 + this.sendResetPasswordEmail(passwordResetLink, email);
  232 + } catch (ThingsboardException e) {
  233 + log.error("Error occurred: {} ", e.getMessage());
  234 + }
  235 + });
  236 + }
  237 +
  238 + @Override
225 239 public void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException {
226 240
227 241 String subject = messages.getMessage("password.was.reset.subject", null, Locale.US);
... ...
... ... @@ -206,11 +206,7 @@ public abstract class AbstractOAuth2ClientMapper {
206 206 }
207 207
208 208 private Optional<DashboardId> getDashboardId(TenantId tenantId, String dashboardName) {
209   - PageLink searchTextLink = new PageLink(1, 0, dashboardName);
210   - PageData<DashboardInfo> dashboardsPage = dashboardService.findDashboardsByTenantId(tenantId, searchTextLink);
211   - return dashboardsPage.getData().stream()
212   - .findAny()
213   - .map(IdBased::getId);
  209 + return Optional.ofNullable(dashboardService.findFirstDashboardInfoByTenantIdAndName(tenantId, dashboardName)).map(IdBased::getId);
214 210 }
215 211
216 212 private Optional<DashboardId> getDashboardId(TenantId tenantId, CustomerId customerId, String dashboardName) {
... ...
... ... @@ -155,6 +155,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
155 155
156 156 doPost("/api/noauth/resetPasswordByEmail", resetPasswordByEmailRequest)
157 157 .andExpect(status().isOk());
  158 + Thread.sleep(1000);
158 159 doGet("/api/noauth/resetPassword?resetToken={resetToken}", TestMailService.currentResetPasswordToken)
159 160 .andExpect(status().isSeeOther())
160 161 .andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + TestMailService.currentResetPasswordToken));
... ...
... ... @@ -51,7 +51,7 @@ public class TestMailService {
51 51 currentResetPasswordToken = passwordResetLink.split("=")[1];
52 52 return null;
53 53 }
54   - }).when(mailService).sendResetPasswordEmail(Mockito.anyString(), Mockito.anyString());
  54 + }).when(mailService).sendResetPasswordEmailAsync(Mockito.anyString(), Mockito.anyString());
55 55 return mailService;
56 56 }
57 57
... ...
... ... @@ -58,4 +58,6 @@ public interface DashboardService {
58 58 Dashboard unassignDashboardFromEdge(TenantId tenantId, DashboardId dashboardId, EdgeId edgeId);
59 59
60 60 PageData<DashboardInfo> findDashboardsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink);
  61 +
  62 + DashboardInfo findFirstDashboardInfoByTenantIdAndName(TenantId tenantId, String name);
61 63 }
... ...
... ... @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.device.profile;
17 17
18 18 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
19 19 import lombok.Data;
  20 +import org.thingsboard.server.common.data.query.FilterPredicateValue;
20 21
21 22 import java.util.concurrent.TimeUnit;
22 23
... ... @@ -25,7 +26,7 @@ import java.util.concurrent.TimeUnit;
25 26 public class DurationAlarmConditionSpec implements AlarmConditionSpec {
26 27
27 28 private TimeUnit unit;
28   - private long value;
  29 + private FilterPredicateValue<Long> predicate;
29 30
30 31 @Override
31 32 public AlarmConditionSpecType getType() {
... ...
... ... @@ -17,14 +17,13 @@ package org.thingsboard.server.common.data.device.profile;
17 17
18 18 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
19 19 import lombok.Data;
20   -
21   -import java.util.concurrent.TimeUnit;
  20 +import org.thingsboard.server.common.data.query.FilterPredicateValue;
22 21
23 22 @Data
24 23 @JsonIgnoreProperties(ignoreUnknown = true)
25 24 public class RepeatingAlarmConditionSpec implements AlarmConditionSpec {
26 25
27   - private int count;
  26 + private FilterPredicateValue<Integer> predicate;
28 27
29 28 @Override
30 29 public AlarmConditionSpecType getType() {
... ...
... ... @@ -56,4 +56,6 @@ public interface DashboardInfoDao extends Dao<DashboardInfo> {
56 56 */
57 57 PageData<DashboardInfo> findDashboardsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink);
58 58
  59 + DashboardInfo findFirstByTenantIdAndName(UUID tenantId, String name);
  60 +
59 61 }
... ...
... ... @@ -34,7 +34,6 @@ import org.thingsboard.server.common.data.id.EdgeId;
34 34 import org.thingsboard.server.common.data.id.TenantId;
35 35 import org.thingsboard.server.common.data.page.PageData;
36 36 import org.thingsboard.server.common.data.page.PageLink;
37   -import org.thingsboard.server.common.data.page.TimePageLink;
38 37 import org.thingsboard.server.common.data.relation.EntityRelation;
39 38 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
40 39 import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
... ... @@ -269,6 +268,11 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
269 268 return dashboardInfoDao.findDashboardsByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink);
270 269 }
271 270
  271 + @Override
  272 + public DashboardInfo findFirstDashboardInfoByTenantIdAndName(TenantId tenantId, String name) {
  273 + return dashboardInfoDao.findFirstByTenantIdAndName(tenantId.getId(), name);
  274 + }
  275 +
272 276 private DataValidator<Dashboard> dashboardValidator =
273 277 new DataValidator<Dashboard>() {
274 278 @Override
... ...
... ... @@ -29,6 +29,8 @@ import java.util.UUID;
29 29 */
30 30 public interface DashboardInfoRepository extends PagingAndSortingRepository<DashboardInfoEntity, UUID> {
31 31
  32 + DashboardInfoEntity findFirstByTenantIdAndTitle(UUID tenantId, String title);
  33 +
32 34 @Query("SELECT di FROM DashboardInfoEntity di WHERE di.tenantId = :tenantId " +
33 35 "AND LOWER(di.searchText) LIKE LOWER(CONCAT(:searchText, '%'))")
34 36 Page<DashboardInfoEntity> findByTenantId(@Param("tenantId") UUID tenantId,
... ...
... ... @@ -83,4 +83,9 @@ public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao<DashboardInfoE
83 83 Objects.toString(pageLink.getTextSearch(), ""),
84 84 DaoUtil.toPageable(pageLink)));
85 85 }
  86 +
  87 + @Override
  88 + public DashboardInfo findFirstByTenantIdAndName(UUID tenantId, String name) {
  89 + return DaoUtil.getData(dashboardInfoRepository.findFirstByTenantIdAndTitle(tenantId, name));
  90 + }
86 91 }
... ...
... ... @@ -25,7 +25,10 @@ import org.apache.commons.lang3.StringUtils;
25 25 import org.springframework.beans.factory.annotation.Value;
26 26 import org.springframework.context.ApplicationEventPublisher;
27 27 import org.springframework.context.annotation.Lazy;
  28 +import org.springframework.security.authentication.DisabledException;
  29 +import org.springframework.security.core.userdetails.UsernameNotFoundException;
28 30 import org.springframework.stereotype.Service;
  31 +import org.thingsboard.common.util.JacksonUtil;
29 32 import org.thingsboard.server.common.data.Customer;
30 33 import org.thingsboard.server.common.data.EntityType;
31 34 import org.thingsboard.server.common.data.Tenant;
... ... @@ -49,7 +52,6 @@ import org.thingsboard.server.dao.service.DataValidator;
49 52 import org.thingsboard.server.dao.service.PaginatedRemover;
50 53 import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
51 54 import org.thingsboard.server.dao.tenant.TenantDao;
52   -import org.thingsboard.common.util.JacksonUtil;
53 55
54 56 import java.util.HashMap;
55 57 import java.util.Map;
... ... @@ -194,11 +196,11 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
194 196 DataValidator.validateEmail(email);
195 197 User user = userDao.findByEmail(tenantId, email);
196 198 if (user == null) {
197   - throw new IncorrectParameterException(String.format("Unable to find user by email [%s]", email));
  199 + throw new UsernameNotFoundException(String.format("Unable to find user by email [%s]", email));
198 200 }
199 201 UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, user.getUuidId());
200 202 if (!userCredentials.isEnabled()) {
201   - throw new IncorrectParameterException("Unable to reset password for inactive user");
  203 + throw new DisabledException(String.format("User credentials not enabled [%s]", email));
202 204 }
203 205 userCredentials.setResetToken(RandomStringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH));
204 206 return saveUserCredentials(tenantId, userCredentials);
... ... @@ -365,7 +367,8 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
365 367 JsonNode userPasswordHistoryJson;
366 368 if (additionalInfo.has(USER_PASSWORD_HISTORY)) {
367 369 userPasswordHistoryJson = additionalInfo.get(USER_PASSWORD_HISTORY);
368   - userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>(){});
  370 + userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() {
  371 + });
369 372 }
370 373 if (userPasswordHistoryMap != null) {
371 374 userPasswordHistoryMap.put(Long.toString(System.currentTimeMillis()), userCredentials.getPassword());
... ...
... ... @@ -31,22 +31,25 @@ public interface MailService {
31 31 void updateMailConfiguration();
32 32
33 33 void sendEmail(TenantId tenantId, String email, String subject, String message) throws ThingsboardException;
34   -
  34 +
35 35 void sendTestMail(JsonNode config, String email) throws ThingsboardException;
36   -
  36 +
37 37 void sendActivationEmail(String activationLink, String email) throws ThingsboardException;
38   -
  38 +
39 39 void sendAccountActivatedEmail(String loginLink, String email) throws ThingsboardException;
40   -
  40 +
41 41 void sendResetPasswordEmail(String passwordResetLink, String email) throws ThingsboardException;
42 42
  43 + void sendResetPasswordEmailAsync(String passwordResetLink, String email);
  44 +
43 45 void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException;
44 46
45   - void sendAccountLockoutEmail( String lockoutEmail, String email, Integer maxFailedLoginAttempts) throws ThingsboardException;
  47 + void sendAccountLockoutEmail(String lockoutEmail, String email, Integer maxFailedLoginAttempts) throws ThingsboardException;
46 48
47 49 void send(TenantId tenantId, CustomerId customerId, String from, String to, String cc, String bcc, String subject, String body, boolean isHtml, Map<String, String> images) throws ThingsboardException;
48 50
49 51 void send(TenantId tenantId, CustomerId customerId, String from, String to, String cc, String bcc, String subject, String body, boolean isHtml, Map<String, String> images, JavaMailSender javaMailSender) throws ThingsboardException;
50 52
51 53 void sendApiFeatureStateEmail(ApiFeature apiFeature, ApiUsageStateValue stateValue, String email, ApiUsageStateMailMessage msg) throws ThingsboardException;
  54 +
52 55 }
... ...
... ... @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter;
24 24 import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey;
25 25 import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType;
26 26 import org.thingsboard.server.common.data.device.profile.AlarmConditionSpec;
  27 +import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType;
27 28 import org.thingsboard.server.common.data.device.profile.AlarmRule;
28 29 import org.thingsboard.server.common.data.device.profile.CustomTimeSchedule;
29 30 import org.thingsboard.server.common.data.device.profile.CustomTimeScheduleItem;
... ... @@ -33,6 +34,7 @@ import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpe
33 34 import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule;
34 35 import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
35 36 import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
  37 +import org.thingsboard.server.common.data.query.DynamicValue;
36 38 import org.thingsboard.server.common.data.query.FilterPredicateValue;
37 39 import org.thingsboard.server.common.data.query.KeyFilterPredicate;
38 40 import org.thingsboard.server.common.data.query.NumericFilterPredicate;
... ... @@ -43,6 +45,7 @@ import java.time.Instant;
43 45 import java.time.ZoneId;
44 46 import java.time.ZonedDateTime;
45 47 import java.util.Set;
  48 +import java.util.concurrent.TimeUnit;
46 49 import java.util.function.Function;
47 50
48 51 @Data
... ... @@ -52,8 +55,6 @@ class AlarmRuleState {
52 55 private final AlarmSeverity severity;
53 56 private final AlarmRule alarmRule;
54 57 private final AlarmConditionSpec spec;
55   - private final long requiredDurationInMs;
56   - private final long requiredRepeats;
57 58 private final Set<AlarmConditionFilterKey> entityKeys;
58 59 private PersistedAlarmRuleState state;
59 60 private boolean updateFlag;
... ... @@ -69,20 +70,6 @@ class AlarmRuleState {
69 70 this.state = new PersistedAlarmRuleState(0L, 0L, 0L);
70 71 }
71 72 this.spec = getSpec(alarmRule);
72   - long requiredDurationInMs = 0;
73   - long requiredRepeats = 0;
74   - switch (spec.getType()) {
75   - case DURATION:
76   - DurationAlarmConditionSpec duration = (DurationAlarmConditionSpec) spec;
77   - requiredDurationInMs = duration.getUnit().toMillis(duration.getValue());
78   - break;
79   - case REPEATING:
80   - RepeatingAlarmConditionSpec repeating = (RepeatingAlarmConditionSpec) spec;
81   - requiredRepeats = repeating.getCount();
82   - break;
83   - }
84   - this.requiredDurationInMs = requiredDurationInMs;
85   - this.requiredRepeats = requiredRepeats;
86 73 this.dynamicPredicateValueCtx = dynamicPredicateValueCtx;
87 74 }
88 75
... ... @@ -211,6 +198,7 @@ class AlarmRuleState {
211 198 if (active && eval(alarmRule.getCondition(), data)) {
212 199 state.setEventCount(state.getEventCount() + 1);
213 200 updateFlag = true;
  201 + long requiredRepeats = resolveRequiredRepeats(data);
214 202 return state.getEventCount() >= requiredRepeats ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE;
215 203 } else {
216 204 return AlarmEvalResult.FALSE;
... ... @@ -230,18 +218,62 @@ class AlarmRuleState {
230 218 state.setDuration(0L);
231 219 updateFlag = true;
232 220 }
  221 + long requiredDurationInMs = resolveRequiredDurationInMs(data);
233 222 return state.getDuration() > requiredDurationInMs ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE;
234 223 } else {
235 224 return AlarmEvalResult.FALSE;
236 225 }
237 226 }
238 227
239   - public AlarmEvalResult eval(long ts) {
  228 + private long resolveRequiredRepeats(DataSnapshot data) {
  229 + long repeatingTimes = 0;
  230 + AlarmConditionSpec alarmConditionSpec = getSpec();
  231 + AlarmConditionSpecType specType = alarmConditionSpec.getType();
  232 + if(specType.equals(AlarmConditionSpecType.REPEATING)) {
  233 + RepeatingAlarmConditionSpec repeating = (RepeatingAlarmConditionSpec) spec;
  234 +
  235 + repeatingTimes = repeating.getPredicate().getDefaultValue();
  236 +
  237 + if (repeating.getPredicate().getDynamicValue() != null &&
  238 + repeating.getPredicate().getDynamicValue().getSourceAttribute() != null) {
  239 + EntityKeyValue repeatingKeyValue = getDynamicPredicateValue(data, repeating.getPredicate().getDynamicValue());
  240 + if (repeatingKeyValue != null) {
  241 + repeatingTimes = repeatingKeyValue.getLngValue();
  242 + }
  243 + }
  244 + }
  245 + return repeatingTimes;
  246 + }
  247 +
  248 + private long resolveRequiredDurationInMs(DataSnapshot data) {
  249 + long durationTimeInMs = 0;
  250 + AlarmConditionSpec alarmConditionSpec = getSpec();
  251 + AlarmConditionSpecType specType = alarmConditionSpec.getType();
  252 + if(specType.equals(AlarmConditionSpecType.DURATION)) {
  253 + DurationAlarmConditionSpec duration = (DurationAlarmConditionSpec) spec;
  254 + TimeUnit timeUnit = duration.getUnit();
  255 +
  256 + durationTimeInMs = timeUnit.toMillis(duration.getPredicate().getDefaultValue());
  257 +
  258 + if (duration.getPredicate().getDynamicValue() != null &&
  259 + duration.getPredicate().getDynamicValue().getSourceAttribute() != null) {
  260 + EntityKeyValue durationKeyValue = getDynamicPredicateValue(data, duration.getPredicate().getDynamicValue());
  261 + if (durationKeyValue != null) {
  262 + durationTimeInMs = timeUnit.toMillis(durationKeyValue.getLngValue());
  263 + }
  264 + }
  265 + }
  266 +
  267 + return durationTimeInMs;
  268 + }
  269 +
  270 + public AlarmEvalResult eval(long ts, DataSnapshot dataSnapshot) {
240 271 switch (spec.getType()) {
241 272 case SIMPLE:
242 273 case REPEATING:
243 274 return AlarmEvalResult.NOT_YET_TRUE;
244 275 case DURATION:
  276 + long requiredDurationInMs = resolveRequiredDurationInMs(dataSnapshot);
245 277 if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && ts > state.getLastEventTs()) {
246 278 long duration = state.getDuration() + (ts - state.getLastEventTs());
247 279 if (isActive(ts)) {
... ... @@ -411,7 +443,7 @@ class AlarmRuleState {
411 443 }
412 444
413 445 private <T> T getPredicateValue(DataSnapshot data, FilterPredicateValue<T> value, AlarmConditionFilter filter, Function<EntityKeyValue, T> transformFunction) {
414   - EntityKeyValue ekv = getDynamicPredicateValue(data, value);
  446 + EntityKeyValue ekv = getDynamicPredicateValue(data, value.getDynamicValue());
415 447 if (ekv != null) {
416 448 T result = transformFunction.apply(ekv);
417 449 if (result != null) {
... ... @@ -425,22 +457,22 @@ class AlarmRuleState {
425 457 }
426 458 }
427 459
428   - private <T> EntityKeyValue getDynamicPredicateValue(DataSnapshot data, FilterPredicateValue<T> value) {
  460 + private <T> EntityKeyValue getDynamicPredicateValue(DataSnapshot data, DynamicValue<T> value) {
429 461 EntityKeyValue ekv = null;
430   - if (value.getDynamicValue() != null) {
431   - switch (value.getDynamicValue().getSourceType()) {
  462 + if (value != null) {
  463 + switch (value.getSourceType()) {
432 464 case CURRENT_DEVICE:
433   - ekv = data.getValue(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, value.getDynamicValue().getSourceAttribute()));
434   - if (ekv != null || !value.getDynamicValue().isInherit()) {
  465 + ekv = data.getValue(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, value.getSourceAttribute()));
  466 + if (ekv != null || !value.isInherit()) {
435 467 break;
436 468 }
437 469 case CURRENT_CUSTOMER:
438   - ekv = dynamicPredicateValueCtx.getCustomerValue(value.getDynamicValue().getSourceAttribute());
439   - if (ekv != null || !value.getDynamicValue().isInherit()) {
  470 + ekv = dynamicPredicateValueCtx.getCustomerValue(value.getSourceAttribute());
  471 + if (ekv != null || !value.isInherit()) {
440 472 break;
441 473 }
442 474 case CURRENT_TENANT:
443   - ekv = dynamicPredicateValueCtx.getTenantValue(value.getDynamicValue().getSourceAttribute());
  475 + ekv = dynamicPredicateValueCtx.getTenantValue(value.getSourceAttribute());
444 476 }
445 477 }
446 478 return ekv;
... ...
... ... @@ -80,7 +80,7 @@ class AlarmState {
80 80
81 81 public boolean process(TbContext ctx, long ts) throws ExecutionException, InterruptedException {
82 82 initCurrentAlarm(ctx);
83   - return createOrClearAlarms(ctx, null, ts, null, AlarmRuleState::eval);
  83 + return createOrClearAlarms(ctx, null, ts, null, (alarmState, tsParam) -> alarmState.eval(tsParam, dataSnapshot));
84 84 }
85 85
86 86 public <T> boolean createOrClearAlarms(TbContext ctx, TbMsg msg, T data, SnapshotUpdate update, BiFunction<AlarmRuleState, T, AlarmEvalResult> evalFunction) {
... ...
... ... @@ -19,24 +19,21 @@ import lombok.AccessLevel;
19 19 import lombok.Getter;
20 20 import org.thingsboard.server.common.data.DeviceProfile;
21 21 import org.thingsboard.server.common.data.alarm.AlarmSeverity;
22   -import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter;
23 22 import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey;
24 23 import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType;
  24 +import org.thingsboard.server.common.data.device.profile.AlarmConditionSpec;
  25 +import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType;
25 26 import org.thingsboard.server.common.data.device.profile.AlarmRule;
26 27 import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
  28 +import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec;
  29 +import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec;
27 30 import org.thingsboard.server.common.data.id.DeviceProfileId;
28 31 import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
29 32 import org.thingsboard.server.common.data.query.DynamicValue;
30 33 import org.thingsboard.server.common.data.query.DynamicValueSourceType;
31   -import org.thingsboard.server.common.data.query.EntityKey;
32   -import org.thingsboard.server.common.data.query.EntityKeyType;
33   -import org.thingsboard.server.common.data.query.FilterPredicateValue;
34   -import org.thingsboard.server.common.data.query.KeyFilter;
35 34 import org.thingsboard.server.common.data.query.KeyFilterPredicate;
36 35 import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate;
37   -import org.thingsboard.server.common.data.query.StringFilterPredicate;
38 36
39   -import javax.print.attribute.standard.Severity;
40 37 import java.util.Collections;
41 38 import java.util.HashMap;
42 39 import java.util.HashSet;
... ... @@ -79,6 +76,7 @@ class ProfileState {
79 76 ruleKeys.add(keyFilter.getKey());
80 77 addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, ruleKeys);
81 78 }
  79 + addEntityKeysFromAlarmConditionSpec(alarmRule);
82 80 }));
83 81 if (alarm.getClearRule() != null) {
84 82 var clearAlarmKeys = alarmClearKeys.computeIfAbsent(alarm.getId(), id -> new HashSet<>());
... ... @@ -87,11 +85,43 @@ class ProfileState {
87 85 clearAlarmKeys.add(keyFilter.getKey());
88 86 addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, clearAlarmKeys);
89 87 }
  88 + addEntityKeysFromAlarmConditionSpec(alarm.getClearRule());
90 89 }
91 90 }
92 91 }
93 92 }
94 93
  94 + private void addEntityKeysFromAlarmConditionSpec(AlarmRule alarmRule) {
  95 + AlarmConditionSpec spec = alarmRule.getCondition().getSpec();
  96 + if (spec == null) {
  97 + return;
  98 + }
  99 + AlarmConditionSpecType specType = spec.getType();
  100 + switch (specType) {
  101 + case DURATION:
  102 + DurationAlarmConditionSpec duration = (DurationAlarmConditionSpec) spec;
  103 + if(duration.getPredicate().getDynamicValue() != null
  104 + && duration.getPredicate().getDynamicValue().getSourceAttribute() != null) {
  105 + entityKeys.add(
  106 + new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE,
  107 + duration.getPredicate().getDynamicValue().getSourceAttribute())
  108 + );
  109 + }
  110 + break;
  111 + case REPEATING:
  112 + RepeatingAlarmConditionSpec repeating = (RepeatingAlarmConditionSpec) spec;
  113 + if(repeating.getPredicate().getDynamicValue() != null
  114 + && repeating.getPredicate().getDynamicValue().getSourceAttribute() != null) {
  115 + entityKeys.add(
  116 + new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE,
  117 + repeating.getPredicate().getDynamicValue().getSourceAttribute())
  118 + );
  119 + }
  120 + break;
  121 + }
  122 +
  123 + }
  124 +
95 125 private void addDynamicValuesRecursively(KeyFilterPredicate predicate, Set<AlarmConditionFilterKey> entityKeys, Set<AlarmConditionFilterKey> ruleKeys) {
96 126 switch (predicate.getType()) {
97 127 case STRING:
... ...
... ... @@ -174,7 +174,13 @@ public class TbHttpClient {
174 174 String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg);
175 175 HttpHeaders headers = prepareHeaders(msg);
176 176 HttpMethod method = HttpMethod.valueOf(config.getRequestMethod());
177   - HttpEntity<String> entity = new HttpEntity<>(msg.getData(), headers);
  177 + HttpEntity<String> entity;
  178 + if(HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method) ||
  179 + HttpMethod.OPTIONS.equals(method) || HttpMethod.TRACE.equals(method)) {
  180 + entity = new HttpEntity<>(headers);
  181 + } else {
  182 + entity = new HttpEntity<>(msg.getData(), headers);
  183 + }
178 184
179 185 ListenableFuture<ResponseEntity<String>> future = httpClient.exchange(
180 186 endpointUrl, method, entity, String.class);
... ...
... ... @@ -42,6 +42,8 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType;
42 42 import org.thingsboard.server.common.data.device.profile.AlarmRule;
43 43 import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
44 44 import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
  45 +import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec;
  46 +import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec;
45 47 import org.thingsboard.server.common.data.id.CustomerId;
46 48 import org.thingsboard.server.common.data.id.DeviceId;
47 49 import org.thingsboard.server.common.data.id.DeviceProfileId;
... ... @@ -64,12 +66,15 @@ import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey;
64 66 import org.thingsboard.server.dao.model.sql.AttributeKvEntity;
65 67 import org.thingsboard.server.dao.timeseries.TimeseriesService;
66 68
  69 +import java.math.BigDecimal;
  70 +import java.math.RoundingMode;
67 71 import java.util.Arrays;
68 72 import java.util.Collections;
69 73 import java.util.List;
70 74 import java.util.Optional;
71 75 import java.util.TreeMap;
72 76 import java.util.UUID;
  77 +import java.util.concurrent.TimeUnit;
73 78
74 79 import static org.mockito.ArgumentMatchers.eq;
75 80 import static org.mockito.Mockito.verify;
... ... @@ -94,10 +99,10 @@ public class TbDeviceProfileNodeTest {
94 99 @Mock
95 100 private AttributesService attributesService;
96 101
97   - private TenantId tenantId = new TenantId(UUID.randomUUID());
98   - private DeviceId deviceId = new DeviceId(UUID.randomUUID());
99   - private CustomerId customerId = new CustomerId(UUID.randomUUID());
100   - private DeviceProfileId deviceProfileId = new DeviceProfileId(UUID.randomUUID());
  102 + private final TenantId tenantId = new TenantId(UUID.randomUUID());
  103 + private final DeviceId deviceId = new DeviceId(UUID.randomUUID());
  104 + private final CustomerId customerId = new CustomerId(UUID.randomUUID());
  105 + private final DeviceProfileId deviceProfileId = new DeviceProfileId(UUID.randomUUID());
101 106
102 107 @Test
103 108 public void testRandomMessageType() throws Exception {
... ... @@ -445,6 +450,642 @@ public class TbDeviceProfileNodeTest {
445 450 }
446 451
447 452 @Test
  453 + public void testCurrentDeviceAttributeForDynamicDurationValue() throws Exception {
  454 + init();
  455 +
  456 + DeviceProfile deviceProfile = new DeviceProfile();
  457 + deviceProfile.setId(deviceProfileId);
  458 + DeviceProfileData deviceProfileData = new DeviceProfileData();
  459 +
  460 + AttributeKvCompositeKey compositeKey = new AttributeKvCompositeKey(
  461 + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "greaterAttribute"
  462 + );
  463 +
  464 + AttributeKvEntity attributeKvEntity = new AttributeKvEntity();
  465 + attributeKvEntity.setId(compositeKey);
  466 + attributeKvEntity.setLongValue(30L);
  467 + attributeKvEntity.setLastUpdateTs(0L);
  468 +
  469 + AttributeKvCompositeKey alarmDelayCompositeKey = new AttributeKvCompositeKey(
  470 + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "alarm_delay"
  471 + );
  472 +
  473 + AttributeKvEntity alarmDelayAttributeKvEntity = new AttributeKvEntity();
  474 + alarmDelayAttributeKvEntity.setId(alarmDelayCompositeKey);
  475 + long alarmDelayInSeconds = 5L;
  476 + alarmDelayAttributeKvEntity.setLongValue(alarmDelayInSeconds);
  477 + alarmDelayAttributeKvEntity.setLastUpdateTs(0L);
  478 +
  479 + AttributeKvEntry entry = attributeKvEntity.toData();
  480 +
  481 + AttributeKvEntry alarmDelayAttributeKvEntry = alarmDelayAttributeKvEntity.toData();
  482 +
  483 + ListenableFuture<List<AttributeKvEntry>> listListenableFuture =
  484 + Futures.immediateFuture(Arrays.asList(entry, alarmDelayAttributeKvEntry));
  485 +
  486 + AlarmConditionFilter highTempFilter = new AlarmConditionFilter();
  487 + highTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature"));
  488 + highTempFilter.setValueType(EntityKeyValueType.NUMERIC);
  489 + NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate();
  490 + highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
  491 + highTemperaturePredicate.setValue(new FilterPredicateValue<>(
  492 + 0.0,
  493 + null,
  494 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "greaterAttribute", false)
  495 + ));
  496 + highTempFilter.setPredicate(highTemperaturePredicate);
  497 + AlarmCondition alarmCondition = new AlarmCondition();
  498 + alarmCondition.setCondition(Collections.singletonList(highTempFilter));
  499 +
  500 + FilterPredicateValue<Long> filterPredicateValue = new FilterPredicateValue<>(
  501 + 10L,
  502 + null,
  503 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "alarm_delay", false)
  504 + );
  505 +
  506 + DurationAlarmConditionSpec durationSpec = new DurationAlarmConditionSpec();
  507 + durationSpec.setUnit(TimeUnit.SECONDS);
  508 + durationSpec.setPredicate(filterPredicateValue);
  509 + alarmCondition.setSpec(durationSpec);
  510 +
  511 + AlarmRule alarmRule = new AlarmRule();
  512 + alarmRule.setCondition(alarmCondition);
  513 + DeviceProfileAlarm dpa = new DeviceProfileAlarm();
  514 + dpa.setId("highTemperatureAlarmID");
  515 + dpa.setAlarmType("highTemperatureAlarm");
  516 + dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule)));
  517 +
  518 + deviceProfileData.setAlarms(Collections.singletonList(dpa));
  519 + deviceProfile.setProfileData(deviceProfileData);
  520 +
  521 + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile);
  522 + Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature")))
  523 + .thenReturn(Futures.immediateFuture(Collections.emptyList()));
  524 + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm"))
  525 + .thenReturn(Futures.immediateFuture(null));
  526 + Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg());
  527 + Mockito.when(ctx.getAttributesService()).thenReturn(attributesService);
  528 + Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet()))
  529 + .thenReturn(listListenableFuture);
  530 +
  531 + TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "");
  532 + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString()))
  533 + .thenReturn(theMsg);
  534 +
  535 + ObjectNode data = mapper.createObjectNode();
  536 + data.put("temperature", 35);
  537 + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(),
  538 + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
  539 +
  540 + node.onMsg(ctx, msg);
  541 + verify(ctx).tellSuccess(msg);
  542 + int halfOfAlarmDelay = new BigDecimal(alarmDelayInSeconds)
  543 + .multiply(BigDecimal.valueOf(1000))
  544 + .divide(BigDecimal.valueOf(2), 3, RoundingMode.HALF_EVEN)
  545 + .intValueExact();
  546 + Thread.sleep(halfOfAlarmDelay);
  547 +
  548 + verify(ctx, Mockito.never()).tellNext(theMsg, "Alarm Created");
  549 +
  550 + Thread.sleep(halfOfAlarmDelay);
  551 +
  552 + TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(),
  553 + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
  554 +
  555 + node.onMsg(ctx, msg2);
  556 + verify(ctx).tellSuccess(msg2);
  557 + verify(ctx).tellNext(theMsg, "Alarm Created");
  558 + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
  559 + }
  560 +
  561 + @Test
  562 + public void testInheritTenantAttributeForDuration() throws Exception {
  563 + init();
  564 +
  565 + DeviceProfile deviceProfile = new DeviceProfile();
  566 + deviceProfile.setId(deviceProfileId);
  567 + DeviceProfileData deviceProfileData = new DeviceProfileData();
  568 +
  569 + Device device = new Device();
  570 + device.setId(deviceId);
  571 + device.setCustomerId(customerId);
  572 +
  573 +
  574 + AttributeKvCompositeKey compositeKey = new AttributeKvCompositeKey(
  575 + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "greaterAttribute"
  576 + );
  577 +
  578 + AttributeKvEntity attributeKvEntity = new AttributeKvEntity();
  579 + attributeKvEntity.setId(compositeKey);
  580 + attributeKvEntity.setLongValue(30L);
  581 + attributeKvEntity.setLastUpdateTs(0L);
  582 +
  583 + AttributeKvCompositeKey alarmDelayCompositeKey = new AttributeKvCompositeKey(
  584 + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "alarm_delay"
  585 + );
  586 +
  587 + AttributeKvEntity alarmDelayAttributeKvEntity = new AttributeKvEntity();
  588 + alarmDelayAttributeKvEntity.setId(alarmDelayCompositeKey);
  589 + long alarmDelayInSeconds = 5L;
  590 + alarmDelayAttributeKvEntity.setLongValue(alarmDelayInSeconds);
  591 + alarmDelayAttributeKvEntity.setLastUpdateTs(0L);
  592 +
  593 + AttributeKvEntry entry = attributeKvEntity.toData();
  594 +
  595 + AttributeKvEntry alarmDelayAttributeKvEntry = alarmDelayAttributeKvEntity.toData();
  596 +
  597 + ListenableFuture<Optional<AttributeKvEntry>> optionalDurationAttribute =
  598 + Futures.immediateFuture(Optional.of(alarmDelayAttributeKvEntry));
  599 + ListenableFuture<List<AttributeKvEntry>> listNoDurationAttribute =
  600 + Futures.immediateFuture(Collections.singletonList(entry));
  601 + ListenableFuture<Optional<AttributeKvEntry>> emptyOptional =
  602 + Futures.immediateFuture(Optional.empty());
  603 +
  604 + AlarmConditionFilter highTempFilter = new AlarmConditionFilter();
  605 + highTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature"));
  606 + highTempFilter.setValueType(EntityKeyValueType.NUMERIC);
  607 + NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate();
  608 + highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
  609 + highTemperaturePredicate.setValue(new FilterPredicateValue<>(
  610 + 0.0,
  611 + null,
  612 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "greaterAttribute", false)
  613 + ));
  614 + highTempFilter.setPredicate(highTemperaturePredicate);
  615 + AlarmCondition alarmCondition = new AlarmCondition();
  616 + alarmCondition.setCondition(Collections.singletonList(highTempFilter));
  617 +
  618 + FilterPredicateValue<Long> filterPredicateValue = new FilterPredicateValue<>(
  619 + 10L,
  620 + null,
  621 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "alarm_delay", true)
  622 + );
  623 +
  624 + DurationAlarmConditionSpec durationSpec = new DurationAlarmConditionSpec();
  625 + durationSpec.setUnit(TimeUnit.SECONDS);
  626 + durationSpec.setPredicate(filterPredicateValue);
  627 + alarmCondition.setSpec(durationSpec);
  628 +
  629 + AlarmRule alarmRule = new AlarmRule();
  630 + alarmRule.setCondition(alarmCondition);
  631 + DeviceProfileAlarm dpa = new DeviceProfileAlarm();
  632 + dpa.setId("highTemperatureAlarmID");
  633 + dpa.setAlarmType("highTemperatureAlarm");
  634 + dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule)));
  635 +
  636 + deviceProfileData.setAlarms(Collections.singletonList(dpa));
  637 + deviceProfile.setProfileData(deviceProfileData);
  638 +
  639 + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile);
  640 + Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature")))
  641 + .thenReturn(Futures.immediateFuture(Collections.emptyList()));
  642 + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm"))
  643 + .thenReturn(Futures.immediateFuture(null));
  644 + Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg());
  645 + Mockito.when(ctx.getAttributesService()).thenReturn(attributesService);
  646 + Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.anyString(), Mockito.anyString()))
  647 + .thenReturn(optionalDurationAttribute);
  648 + Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId))
  649 + .thenReturn(device);
  650 + Mockito.when(attributesService.find(eq(tenantId), eq(customerId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString()))
  651 + .thenReturn(emptyOptional);
  652 + Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet()))
  653 + .thenReturn(listNoDurationAttribute);
  654 +
  655 + TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "");
  656 + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString()))
  657 + .thenReturn(theMsg);
  658 +
  659 + ObjectNode data = mapper.createObjectNode();
  660 + data.put("temperature", 150);
  661 + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(),
  662 + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
  663 +
  664 + node.onMsg(ctx, msg);
  665 + verify(ctx).tellSuccess(msg);
  666 + int halfOfAlarmDelay = new BigDecimal(alarmDelayInSeconds)
  667 + .multiply(BigDecimal.valueOf(1000))
  668 + .divide(BigDecimal.valueOf(2), 3, RoundingMode.HALF_EVEN)
  669 + .intValueExact();
  670 + Thread.sleep(halfOfAlarmDelay);
  671 +
  672 + verify(ctx, Mockito.never()).tellNext(theMsg, "Alarm Created");
  673 +
  674 + Thread.sleep(halfOfAlarmDelay);
  675 +
  676 + TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(),
  677 + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
  678 +
  679 + node.onMsg(ctx, msg2);
  680 + verify(ctx).tellSuccess(msg2);
  681 + verify(ctx).tellNext(theMsg, "Alarm Created");
  682 + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
  683 + }
  684 +
  685 + @Test
  686 + public void testCurrentDeviceAttributeForDynamicRepeatingValue() throws Exception {
  687 + init();
  688 +
  689 + DeviceProfile deviceProfile = new DeviceProfile();
  690 + deviceProfile.setId(deviceProfileId);
  691 + DeviceProfileData deviceProfileData = new DeviceProfileData();
  692 +
  693 + AttributeKvCompositeKey compositeKey = new AttributeKvCompositeKey(
  694 + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "greaterAttribute"
  695 + );
  696 +
  697 + AttributeKvEntity attributeKvEntity = new AttributeKvEntity();
  698 + attributeKvEntity.setId(compositeKey);
  699 + attributeKvEntity.setLongValue(30L);
  700 + attributeKvEntity.setLastUpdateTs(0L);
  701 +
  702 + AttributeKvCompositeKey alarmDelayCompositeKey = new AttributeKvCompositeKey(
  703 + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "alarm_delay"
  704 + );
  705 +
  706 + AttributeKvEntity alarmDelayAttributeKvEntity = new AttributeKvEntity();
  707 + alarmDelayAttributeKvEntity.setId(alarmDelayCompositeKey);
  708 + long alarmRepeating = 2;
  709 + alarmDelayAttributeKvEntity.setLongValue(alarmRepeating);
  710 + alarmDelayAttributeKvEntity.setLastUpdateTs(0L);
  711 +
  712 + AttributeKvEntry entry = attributeKvEntity.toData();
  713 +
  714 + AttributeKvEntry alarmDelayAttributeKvEntry = alarmDelayAttributeKvEntity.toData();
  715 +
  716 + ListenableFuture<List<AttributeKvEntry>> listListenableFuture =
  717 + Futures.immediateFuture(Arrays.asList(entry, alarmDelayAttributeKvEntry));
  718 +
  719 + AlarmConditionFilter highTempFilter = new AlarmConditionFilter();
  720 + highTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature"));
  721 + highTempFilter.setValueType(EntityKeyValueType.NUMERIC);
  722 + NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate();
  723 + highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
  724 + highTemperaturePredicate.setValue(new FilterPredicateValue<>(
  725 + 0.0,
  726 + null,
  727 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "greaterAttribute", false)
  728 + ));
  729 + highTempFilter.setPredicate(highTemperaturePredicate);
  730 + AlarmCondition alarmCondition = new AlarmCondition();
  731 + alarmCondition.setCondition(Collections.singletonList(highTempFilter));
  732 +
  733 + FilterPredicateValue<Integer> filterPredicateValue = new FilterPredicateValue<>(
  734 + 10,
  735 + null,
  736 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "alarm_delay", false)
  737 + );
  738 +
  739 +
  740 + RepeatingAlarmConditionSpec repeatingSpec = new RepeatingAlarmConditionSpec();
  741 + repeatingSpec.setPredicate(filterPredicateValue);
  742 + alarmCondition.setSpec(repeatingSpec);
  743 +
  744 + AlarmRule alarmRule = new AlarmRule();
  745 + alarmRule.setCondition(alarmCondition);
  746 + DeviceProfileAlarm dpa = new DeviceProfileAlarm();
  747 + dpa.setId("highTemperatureAlarmID");
  748 + dpa.setAlarmType("highTemperatureAlarm");
  749 + dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule)));
  750 +
  751 + deviceProfileData.setAlarms(Collections.singletonList(dpa));
  752 + deviceProfile.setProfileData(deviceProfileData);
  753 +
  754 + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile);
  755 + Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature")))
  756 + .thenReturn(Futures.immediateFuture(Collections.emptyList()));
  757 + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm"))
  758 + .thenReturn(Futures.immediateFuture(null));
  759 + Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg());
  760 + Mockito.when(ctx.getAttributesService()).thenReturn(attributesService);
  761 + Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet()))
  762 + .thenReturn(listListenableFuture);
  763 +
  764 + TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "");
  765 + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString()))
  766 + .thenReturn(theMsg);
  767 +
  768 + ObjectNode data = mapper.createObjectNode();
  769 + data.put("temperature", 150);
  770 + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(),
  771 + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
  772 +
  773 + node.onMsg(ctx, msg);
  774 + verify(ctx).tellSuccess(msg);
  775 +
  776 + verify(ctx, Mockito.never()).tellNext(theMsg, "Alarm Created");
  777 +
  778 + TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(),
  779 + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
  780 +
  781 + node.onMsg(ctx, msg2);
  782 + verify(ctx).tellSuccess(msg2);
  783 + verify(ctx).tellNext(theMsg, "Alarm Created");
  784 + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
  785 + }
  786 +
  787 + @Test
  788 + public void testInheritTenantAttributeForRepeating() throws Exception {
  789 + init();
  790 +
  791 + DeviceProfile deviceProfile = new DeviceProfile();
  792 + deviceProfile.setId(deviceProfileId);
  793 + DeviceProfileData deviceProfileData = new DeviceProfileData();
  794 +
  795 + AttributeKvCompositeKey compositeKey = new AttributeKvCompositeKey(
  796 + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "greaterAttribute"
  797 + );
  798 +
  799 + Device device = new Device();
  800 + device.setId(deviceId);
  801 + device.setCustomerId(customerId);
  802 +
  803 + AttributeKvEntity attributeKvEntity = new AttributeKvEntity();
  804 + attributeKvEntity.setId(compositeKey);
  805 + attributeKvEntity.setLongValue(30L);
  806 + attributeKvEntity.setLastUpdateTs(0L);
  807 +
  808 + AttributeKvCompositeKey alarmDelayCompositeKey = new AttributeKvCompositeKey(
  809 + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "alarm_delay"
  810 + );
  811 +
  812 + AttributeKvEntity alarmDelayAttributeKvEntity = new AttributeKvEntity();
  813 + alarmDelayAttributeKvEntity.setId(alarmDelayCompositeKey);
  814 + long repeatingCondition = 2;
  815 + alarmDelayAttributeKvEntity.setLongValue(repeatingCondition);
  816 + alarmDelayAttributeKvEntity.setLastUpdateTs(0L);
  817 +
  818 + AttributeKvEntry entry = attributeKvEntity.toData();
  819 +
  820 + AttributeKvEntry alarmDelayAttributeKvEntry = alarmDelayAttributeKvEntity.toData();
  821 +
  822 + ListenableFuture<Optional<AttributeKvEntry>> optionalDurationAttribute =
  823 + Futures.immediateFuture(Optional.of(alarmDelayAttributeKvEntry));
  824 + ListenableFuture<List<AttributeKvEntry>> listNoDurationAttribute =
  825 + Futures.immediateFuture(Collections.singletonList(entry));
  826 + ListenableFuture<Optional<AttributeKvEntry>> emptyOptional =
  827 + Futures.immediateFuture(Optional.empty());
  828 +
  829 + AlarmConditionFilter highTempFilter = new AlarmConditionFilter();
  830 + highTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature"));
  831 + highTempFilter.setValueType(EntityKeyValueType.NUMERIC);
  832 + NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate();
  833 + highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
  834 + highTemperaturePredicate.setValue(new FilterPredicateValue<>(
  835 + 0.0,
  836 + null,
  837 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "greaterAttribute", false)
  838 + ));
  839 + highTempFilter.setPredicate(highTemperaturePredicate);
  840 + AlarmCondition alarmCondition = new AlarmCondition();
  841 + alarmCondition.setCondition(Collections.singletonList(highTempFilter));
  842 +
  843 + FilterPredicateValue<Integer> filterPredicateValue = new FilterPredicateValue<>(
  844 + 10,
  845 + null,
  846 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "alarm_delay", true)
  847 + );
  848 +
  849 + RepeatingAlarmConditionSpec repeatingSpec = new RepeatingAlarmConditionSpec();
  850 + repeatingSpec.setPredicate(filterPredicateValue);
  851 + alarmCondition.setSpec(repeatingSpec);
  852 +
  853 + AlarmRule alarmRule = new AlarmRule();
  854 + alarmRule.setCondition(alarmCondition);
  855 + DeviceProfileAlarm dpa = new DeviceProfileAlarm();
  856 + dpa.setId("highTemperatureAlarmID");
  857 + dpa.setAlarmType("highTemperatureAlarm");
  858 + dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule)));
  859 +
  860 + deviceProfileData.setAlarms(Collections.singletonList(dpa));
  861 + deviceProfile.setProfileData(deviceProfileData);
  862 +
  863 + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile);
  864 + Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature")))
  865 + .thenReturn(Futures.immediateFuture(Collections.emptyList()));
  866 + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm"))
  867 + .thenReturn(Futures.immediateFuture(null));
  868 + Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg());
  869 + Mockito.when(ctx.getAttributesService()).thenReturn(attributesService);
  870 + Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.anyString(), Mockito.anyString()))
  871 + .thenReturn(optionalDurationAttribute);
  872 + Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId))
  873 + .thenReturn(device);
  874 + Mockito.when(attributesService.find(eq(tenantId), eq(customerId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString()))
  875 + .thenReturn(emptyOptional);
  876 + Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet()))
  877 + .thenReturn(listNoDurationAttribute);
  878 +
  879 + TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "");
  880 + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString()))
  881 + .thenReturn(theMsg);
  882 +
  883 + ObjectNode data = mapper.createObjectNode();
  884 + data.put("temperature", 150);
  885 + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(),
  886 + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
  887 +
  888 + node.onMsg(ctx, msg);
  889 + verify(ctx).tellSuccess(msg);
  890 +
  891 + verify(ctx, Mockito.never()).tellNext(theMsg, "Alarm Created");
  892 +
  893 + TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(),
  894 + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
  895 +
  896 + node.onMsg(ctx, msg2);
  897 + verify(ctx).tellSuccess(msg2);
  898 + verify(ctx).tellNext(theMsg, "Alarm Created");
  899 + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
  900 + }
  901 +
  902 + @Test
  903 + public void testCurrentDeviceAttributeForUseDefaultDurationWhenDynamicDurationValueIsNull() throws Exception {
  904 + init();
  905 +
  906 + long alarmDelayInSeconds = 5;
  907 + DeviceProfile deviceProfile = new DeviceProfile();
  908 + deviceProfile.setId(deviceProfileId);
  909 + DeviceProfileData deviceProfileData = new DeviceProfileData();
  910 +
  911 + Device device = new Device();
  912 + device.setId(deviceId);
  913 + device.setCustomerId(customerId);
  914 +
  915 + AttributeKvCompositeKey compositeKey = new AttributeKvCompositeKey(
  916 + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "greaterAttribute"
  917 + );
  918 +
  919 + AttributeKvEntity attributeKvEntity = new AttributeKvEntity();
  920 + attributeKvEntity.setId(compositeKey);
  921 + attributeKvEntity.setLongValue(30L);
  922 + attributeKvEntity.setLastUpdateTs(0L);
  923 +
  924 + AttributeKvEntry entry = attributeKvEntity.toData();
  925 +
  926 + ListenableFuture<List<AttributeKvEntry>> listListenableFuture =
  927 + Futures.immediateFuture(Collections.singletonList(entry));
  928 +
  929 + AlarmConditionFilter highTempFilter = new AlarmConditionFilter();
  930 + highTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature"));
  931 + highTempFilter.setValueType(EntityKeyValueType.NUMERIC);
  932 + NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate();
  933 + highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
  934 + highTemperaturePredicate.setValue(new FilterPredicateValue<>(
  935 + 0.0,
  936 + null,
  937 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "greaterAttribute")
  938 + ));
  939 + highTempFilter.setPredicate(highTemperaturePredicate);
  940 + AlarmCondition alarmCondition = new AlarmCondition();
  941 + alarmCondition.setCondition(Collections.singletonList(highTempFilter));
  942 +
  943 + FilterPredicateValue<Long> filterPredicateValue = new FilterPredicateValue<>(
  944 + alarmDelayInSeconds,
  945 + null,
  946 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, null, false)
  947 + );
  948 +
  949 + DurationAlarmConditionSpec durationSpec = new DurationAlarmConditionSpec();
  950 + durationSpec.setUnit(TimeUnit.SECONDS);
  951 + durationSpec.setPredicate(filterPredicateValue);
  952 + alarmCondition.setSpec(durationSpec);
  953 +
  954 + AlarmRule alarmRule = new AlarmRule();
  955 + alarmRule.setCondition(alarmCondition);
  956 + DeviceProfileAlarm dpa = new DeviceProfileAlarm();
  957 + dpa.setId("highTemperatureAlarmID");
  958 + dpa.setAlarmType("highTemperatureAlarm");
  959 + dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule)));
  960 +
  961 + deviceProfileData.setAlarms(Collections.singletonList(dpa));
  962 + deviceProfile.setProfileData(deviceProfileData);
  963 +
  964 + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile);
  965 + Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature")))
  966 + .thenReturn(Futures.immediateFuture(Collections.emptyList()));
  967 + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm"))
  968 + .thenReturn(Futures.immediateFuture(null));
  969 + Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg());
  970 + Mockito.when(ctx.getAttributesService()).thenReturn(attributesService);
  971 + Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet()))
  972 + .thenReturn(listListenableFuture);
  973 +
  974 + TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "");
  975 + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString()))
  976 + .thenReturn(theMsg);
  977 +
  978 + ObjectNode data = mapper.createObjectNode();
  979 + data.put("temperature", 35);
  980 + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(),
  981 + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
  982 +
  983 + node.onMsg(ctx, msg);
  984 + verify(ctx).tellSuccess(msg);
  985 + int halfOfAlarmDelay = new BigDecimal(alarmDelayInSeconds)
  986 + .multiply(BigDecimal.valueOf(1000))
  987 + .divide(BigDecimal.valueOf(2), 3, RoundingMode.HALF_EVEN)
  988 + .intValueExact();
  989 + Thread.sleep(halfOfAlarmDelay);
  990 +
  991 + verify(ctx, Mockito.never()).tellNext(theMsg, "Alarm Created");
  992 +
  993 + Thread.sleep(halfOfAlarmDelay);
  994 +
  995 + TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(),
  996 + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
  997 +
  998 + node.onMsg(ctx, msg2);
  999 + verify(ctx).tellSuccess(msg2);
  1000 + verify(ctx).tellNext(theMsg, "Alarm Created");
  1001 + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
  1002 + }
  1003 +
  1004 + @Test
  1005 + public void testCurrentDeviceAttributeForUseDefaultRepeatingWhenDynamicDurationValueIsNull() throws Exception {
  1006 + init();
  1007 +
  1008 + DeviceProfile deviceProfile = new DeviceProfile();
  1009 + deviceProfile.setId(deviceProfileId);
  1010 + DeviceProfileData deviceProfileData = new DeviceProfileData();
  1011 +
  1012 + Device device = new Device();
  1013 + device.setId(deviceId);
  1014 + device.setCustomerId(customerId);
  1015 +
  1016 + AttributeKvCompositeKey compositeKey = new AttributeKvCompositeKey(
  1017 + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "greaterAttribute"
  1018 + );
  1019 +
  1020 + AttributeKvEntity attributeKvEntity = new AttributeKvEntity();
  1021 + attributeKvEntity.setId(compositeKey);
  1022 + attributeKvEntity.setLongValue(30L);
  1023 + attributeKvEntity.setLastUpdateTs(0L);
  1024 +
  1025 + AttributeKvEntry entry = attributeKvEntity.toData();
  1026 +
  1027 + ListenableFuture<List<AttributeKvEntry>> listListenableFuture =
  1028 + Futures.immediateFuture(Collections.singletonList(entry));
  1029 +
  1030 + AlarmConditionFilter highTempFilter = new AlarmConditionFilter();
  1031 + highTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature"));
  1032 + highTempFilter.setValueType(EntityKeyValueType.NUMERIC);
  1033 + NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate();
  1034 + highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
  1035 + highTemperaturePredicate.setValue(new FilterPredicateValue<>(
  1036 + 0.0,
  1037 + null,
  1038 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "greaterAttribute")
  1039 + ));
  1040 + highTempFilter.setPredicate(highTemperaturePredicate);
  1041 + AlarmCondition alarmCondition = new AlarmCondition();
  1042 + alarmCondition.setCondition(Collections.singletonList(highTempFilter));
  1043 +
  1044 + RepeatingAlarmConditionSpec repeating = new RepeatingAlarmConditionSpec();
  1045 + repeating.setPredicate(new FilterPredicateValue<>(
  1046 + 0,
  1047 + null,
  1048 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "alarm_rule", false)
  1049 + ));
  1050 + alarmCondition.setSpec(repeating);
  1051 +
  1052 + AlarmRule alarmRule = new AlarmRule();
  1053 + alarmRule.setCondition(alarmCondition);
  1054 + DeviceProfileAlarm dpa = new DeviceProfileAlarm();
  1055 + dpa.setId("highTemperatureAlarmID");
  1056 + dpa.setAlarmType("highTemperatureAlarm");
  1057 + dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule)));
  1058 +
  1059 + deviceProfileData.setAlarms(Collections.singletonList(dpa));
  1060 + deviceProfile.setProfileData(deviceProfileData);
  1061 +
  1062 + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile);
  1063 + Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature")))
  1064 + .thenReturn(Futures.immediateFuture(Collections.emptyList()));
  1065 + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm"))
  1066 + .thenReturn(Futures.immediateFuture(null));
  1067 + Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg());
  1068 + Mockito.when(ctx.getAttributesService()).thenReturn(attributesService);
  1069 + Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet()))
  1070 + .thenReturn(listListenableFuture);
  1071 +
  1072 + TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "");
  1073 + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString()))
  1074 + .thenReturn(theMsg);
  1075 +
  1076 + ObjectNode data = mapper.createObjectNode();
  1077 + data.put("temperature", 35);
  1078 + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(),
  1079 + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
  1080 +
  1081 + node.onMsg(ctx, msg);
  1082 + verify(ctx).tellSuccess(msg);
  1083 + verify(ctx).tellNext(theMsg, "Alarm Created");
  1084 + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
  1085 + }
  1086 +
  1087 +
  1088 + @Test
448 1089 public void testCurrentCustomersAttributeForDynamicValue() throws Exception {
449 1090 init();
450 1091
... ...
... ... @@ -30,7 +30,9 @@ import {
30 30 DynamicValueSourceType,
31 31 dynamicValueSourceTypeTranslationMap,
32 32 EntityKeyValueType,
33   - FilterPredicateValue
  33 + FilterPredicateValue,
  34 + getDynamicSourcesForAllowUser,
  35 + inheritModeForDynamicValueSourceType
34 36 } from '@shared/models/query/query.models';
35 37
36 38 @Component({
... ... @@ -52,22 +54,14 @@ import {
52 54 })
53 55 export class FilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit {
54 56
55   - private readonly inheritModeForSources: DynamicValueSourceType[] = [
56   - DynamicValueSourceType.CURRENT_CUSTOMER,
57   - DynamicValueSourceType.CURRENT_DEVICE];
  57 + private readonly inheritModeForSources: DynamicValueSourceType[] = inheritModeForDynamicValueSourceType;
58 58
59 59 @Input() disabled: boolean;
60 60
61 61 @Input()
62 62 set allowUserDynamicSource(allow: boolean) {
63   - this.dynamicValueSourceTypes = [DynamicValueSourceType.CURRENT_TENANT,
64   - DynamicValueSourceType.CURRENT_CUSTOMER];
  63 + this.dynamicValueSourceTypes = getDynamicSourcesForAllowUser(allow);
65 64 this.allow = allow;
66   - if (allow) {
67   - this.dynamicValueSourceTypes.push(DynamicValueSourceType.CURRENT_USER);
68   - } else {
69   - this.dynamicValueSourceTypes.push(DynamicValueSourceType.CURRENT_DEVICE);
70   - }
71 65 }
72 66
73 67 private onlyUserDynamicSourceValue = false;
... ... @@ -92,8 +86,9 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, Vali
92 86
93 87 valueTypeEnum = EntityKeyValueType;
94 88
95   - dynamicValueSourceTypes: DynamicValueSourceType[] = [DynamicValueSourceType.CURRENT_TENANT,
96   - DynamicValueSourceType.CURRENT_CUSTOMER, DynamicValueSourceType.CURRENT_USER];
  89 + allow = true;
  90 +
  91 + dynamicValueSourceTypes: DynamicValueSourceType[] = getDynamicSourcesForAllowUser(this.allow);
97 92
98 93 dynamicValueSourceTypeTranslations = dynamicValueSourceTypeTranslationMap;
99 94
... ... @@ -103,8 +98,6 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, Vali
103 98
104 99 inheritMode = false;
105 100
106   - allow = true;
107   -
108 101 private propagateChange = null;
109 102 private propagateChangePending = false;
110 103
... ...
... ... @@ -136,6 +136,7 @@ import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/emb
136 136 import { EdgeDownlinkTableComponent } from '@home/components/edge/edge-downlink-table.component';
137 137 import { EdgeDownlinkTableHeaderComponent } from '@home/components/edge/edge-downlink-table-header.component';
138 138 import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component';
  139 +import { AlarmDurationPredicateValueComponent } from '@home/components/profile/alarm/alarm-duration-predicate-value.component';
139 140 import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component';
140 141 import { WidgetContainerComponent } from '@home/components/widget/widget-container.component';
141 142 import { SnmpDeviceProfileTransportModule } from '@home/components/profile/device/snpm/snmp-device-profile-transport.module';
... ... @@ -239,6 +240,7 @@ import { DeviceCredentialsModule } from '@home/components/device/device-credenti
239 240 AlarmScheduleInfoComponent,
240 241 DeviceProfileProvisionConfigurationComponent,
241 242 AlarmScheduleComponent,
  243 + AlarmDurationPredicateValueComponent,
242 244 DeviceWizardDialogComponent,
243 245 AlarmScheduleDialogComponent,
244 246 EditAlarmDetailsDialogComponent,
... ... @@ -348,6 +350,7 @@ import { DeviceCredentialsModule } from '@home/components/device/device-credenti
348 350 AlarmScheduleInfoComponent,
349 351 AlarmScheduleComponent,
350 352 AlarmScheduleDialogComponent,
  353 + AlarmDurationPredicateValueComponent,
351 354 EditAlarmDetailsDialogComponent,
352 355 DeviceProfileProvisionConfigurationComponent,
353 356 AlarmScheduleComponent,
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="alarmDurationPredicateValueFormGroup">
  19 + <div fxFlex fxLayout="column" [fxShow]="!dynamicMode">
  20 + <mat-form-field floatLabel="always" hideRequiredMarker class="mat-block">
  21 + <mat-label></mat-label>
  22 + <input required type="number" matInput
  23 + step="1" min="1" max="2147483647"
  24 + formControlName="defaultValue"
  25 + placeholder="{{ defaultValuePlaceholder | translate }}">
  26 + <mat-error *ngIf="alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('required')">
  27 + {{ defaultValueRequiredError | translate }}
  28 + </mat-error>
  29 + <mat-error *ngIf="alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('min')">
  30 + {{ defaultValueRangeError | translate }}
  31 + </mat-error>
  32 + <mat-error *ngIf="alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('max')">
  33 + {{ defaultValueRangeError | translate }}
  34 + </mat-error>
  35 + <mat-error *ngIf="alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('pattern')">
  36 + {{ defaultValuePatternError | translate }}
  37 + </mat-error>
  38 + </mat-form-field>
  39 + </div>
  40 + <div fxFlex fxLayout="column" [fxShow]="dynamicMode">
  41 + <div formGroupName="dynamicValue" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
  42 + <div fxFlex="40" fxLayout="column">
  43 + <mat-form-field floatLabel="always" hideRequiredMarker class="mat-block">
  44 + <mat-label></mat-label>
  45 + <mat-select formControlName="sourceType" placeholder="{{'filter.dynamic-source-type' | translate}}">
  46 + <mat-option [value]="null">
  47 + {{'filter.no-dynamic-value' | translate}}
  48 + </mat-option>
  49 + <mat-option *ngFor="let sourceType of dynamicValueSourceTypes" [value]="sourceType">
  50 + {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}}
  51 + </mat-option>
  52 + </mat-select>
  53 + </mat-form-field>
  54 + </div>
  55 + <div fxFlex fxLayout="column">
  56 + <mat-form-field floatLabel="always" hideRequiredMarker class="mat-block source-attribute">
  57 + <mat-label></mat-label>
  58 + <input matInput formControlName="sourceAttribute" placeholder="{{'filter.source-attribute' | translate}}">
  59 + </mat-form-field>
  60 + </div>
  61 + <div *ngIf="inheritMode"
  62 + fxLayout="column"
  63 + style="padding-top: 6px">
  64 + <mat-checkbox formControlName="inherit">
  65 + {{ 'filter.inherit-owner' | translate}}
  66 + </mat-checkbox>
  67 + </div>
  68 + </div>
  69 + </div>
  70 + <button mat-icon-button
  71 + class="mat-elevation-z1 tb-mat-32"
  72 + color="primary"
  73 + type="button"
  74 + matTooltip="{{ (dynamicMode ? 'filter.switch-to-default-value' : 'filter.switch-to-dynamic-value') | translate }}"
  75 + matTooltipPosition="above"
  76 + (click)="dynamicMode = !dynamicMode">
  77 + <mat-icon class="tb-mat-20" [svgIcon]="dynamicMode ? 'mdi:numeric' : 'mdi:variable'"></mat-icon>
  78 + </button>
  79 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +:host ::ng-deep {
  18 + .source-attribute {
  19 + .mat-form-field-infix{
  20 + width: 100%;
  21 + }
  22 + }
  23 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, forwardRef, Input, OnInit } from '@angular/core';
  18 +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
  19 +import {
  20 + DynamicValueSourceType,
  21 + dynamicValueSourceTypeTranslationMap,
  22 + FilterPredicateValue,
  23 + getDynamicSourcesForAllowUser,
  24 + inheritModeForDynamicValueSourceType
  25 +} from '@shared/models/query/query.models';
  26 +import { AlarmConditionType } from '@shared/models/device.models';
  27 +
  28 +@Component({
  29 + selector: 'tb-alarm-duration-predicate-value',
  30 + templateUrl: './alarm-duration-predicate-value.component.html',
  31 + styleUrls: ['./alarm-duration-predicate-value.component.scss'],
  32 + providers: [
  33 + {
  34 + provide: NG_VALUE_ACCESSOR,
  35 + useExisting: forwardRef(() => AlarmDurationPredicateValueComponent),
  36 + multi: true
  37 + }
  38 + ]
  39 +})
  40 +export class AlarmDurationPredicateValueComponent implements ControlValueAccessor, OnInit {
  41 +
  42 + private readonly inheritModeForSources = inheritModeForDynamicValueSourceType;
  43 +
  44 + @Input()
  45 + set alarmConditionType(alarmConditionType: AlarmConditionType) {
  46 + switch (alarmConditionType) {
  47 + case AlarmConditionType.REPEATING:
  48 + this.defaultValuePlaceholder = 'device-profile.condition-repeating-value-required';
  49 + this.defaultValueRequiredError = 'device-profile.condition-repeating-value-range';
  50 + this.defaultValueRangeError = 'device-profile.condition-repeating-value-range';
  51 + this.defaultValuePatternError = 'device-profile.condition-repeating-value-pattern';
  52 + break;
  53 + case AlarmConditionType.DURATION:
  54 + this.defaultValuePlaceholder = 'device-profile.condition-duration-value';
  55 + this.defaultValueRequiredError = 'device-profile.condition-duration-value-required';
  56 + this.defaultValueRangeError = 'device-profile.condition-duration-value-range';
  57 + this.defaultValuePatternError = 'device-profile.condition-duration-value-pattern';
  58 + break;
  59 + }
  60 + }
  61 +
  62 + defaultValuePlaceholder = '';
  63 + defaultValueRequiredError = '';
  64 + defaultValueRangeError = '';
  65 + defaultValuePatternError = '';
  66 +
  67 + dynamicValueSourceTypes: DynamicValueSourceType[] = getDynamicSourcesForAllowUser(false);
  68 +
  69 + dynamicValueSourceTypeTranslations = dynamicValueSourceTypeTranslationMap;
  70 +
  71 + alarmDurationPredicateValueFormGroup: FormGroup;
  72 +
  73 + dynamicMode = false;
  74 +
  75 + inheritMode = false;
  76 +
  77 + private propagateChange = null;
  78 +
  79 + constructor(private fb: FormBuilder) {
  80 + }
  81 +
  82 + ngOnInit(): void {
  83 + this.alarmDurationPredicateValueFormGroup = this.fb.group({
  84 + defaultValue: [0, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]],
  85 + dynamicValue: this.fb.group(
  86 + {
  87 + sourceType: [null],
  88 + sourceAttribute: [null],
  89 + inherit: [false]
  90 + }
  91 + )
  92 + });
  93 + this.alarmDurationPredicateValueFormGroup.get('dynamicValue').get('sourceType').valueChanges.subscribe(
  94 + (sourceType) => {
  95 + if (!sourceType) {
  96 + this.alarmDurationPredicateValueFormGroup.get('dynamicValue').get('sourceAttribute').patchValue(null, {emitEvent: false});
  97 + }
  98 + this.updateShowInheritMode(sourceType);
  99 + }
  100 + );
  101 + this.alarmDurationPredicateValueFormGroup.valueChanges.subscribe(() => {
  102 + this.updateModel();
  103 + });
  104 + }
  105 +
  106 + registerOnChange(fn: any): void {
  107 + this.propagateChange = fn;
  108 + }
  109 +
  110 + registerOnTouched(fn: any): void {
  111 + }
  112 +
  113 + setDisabledState(isDisabled: boolean): void {
  114 + if (isDisabled) {
  115 + this.alarmDurationPredicateValueFormGroup.disable({emitEvent: false});
  116 + } else {
  117 + this.alarmDurationPredicateValueFormGroup.enable({emitEvent: false});
  118 + }
  119 + }
  120 +
  121 + writeValue(predicateValue: FilterPredicateValue<string | number | boolean>): void {
  122 + this.alarmDurationPredicateValueFormGroup.patchValue({
  123 + defaultValue: predicateValue ? predicateValue.defaultValue : null,
  124 + dynamicValue: {
  125 + sourceType: predicateValue?.dynamicValue ? predicateValue.dynamicValue.sourceType : null,
  126 + sourceAttribute: predicateValue?.dynamicValue ? predicateValue.dynamicValue.sourceAttribute : null,
  127 + inherit: predicateValue?.dynamicValue ? predicateValue.dynamicValue.inherit : null
  128 + }
  129 + }, {emitEvent: false});
  130 + }
  131 +
  132 + private updateModel() {
  133 + let predicateValue: FilterPredicateValue<string | number | boolean> = null;
  134 + if (this.alarmDurationPredicateValueFormGroup.valid) {
  135 + predicateValue = this.alarmDurationPredicateValueFormGroup.getRawValue();
  136 + if (predicateValue.dynamicValue) {
  137 + if (!predicateValue.dynamicValue.sourceType || !predicateValue.dynamicValue.sourceAttribute) {
  138 + predicateValue.dynamicValue = null;
  139 + }
  140 + }
  141 + }
  142 + this.propagateChange(predicateValue);
  143 + }
  144 +
  145 + private updateShowInheritMode(sourceType: DynamicValueSourceType) {
  146 + if (this.inheritModeForSources.includes(sourceType)) {
  147 + this.inheritMode = true;
  148 + } else {
  149 + this.alarmDurationPredicateValueFormGroup.get('dynamicValue.inherit').patchValue(false, {emitEvent: false});
  150 + this.inheritMode = false;
  151 + }
  152 + }
  153 +}
... ...
... ... @@ -37,7 +37,7 @@
37 37 [entityId]="entityId"
38 38 formControlName="keyFilters">
39 39 </tb-key-filter-list>
40   - <section formGroupName="spec" class="row">
  40 + <section formGroupName="spec" style="margin-top: 1em">
41 41 <mat-form-field class="mat-block" hideRequiredMarker>
42 42 <mat-label translate>device-profile.condition-type</mat-label>
43 43 <mat-select formControlName="type" required>
... ... @@ -49,60 +49,26 @@
49 49 {{ 'device-profile.condition-type-required' | translate }}
50 50 </mat-error>
51 51 </mat-form-field>
52   - <div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.DURATION">
53   - <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
54   - <mat-label></mat-label>
55   - <input type="number" required
56   - step="1" min="1" max="2147483647" matInput
57   - placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
58   - formControlName="value">
59   - <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('required')">
60   - {{ 'device-profile.condition-duration-value-required' | translate }}
61   - </mat-error>
62   - <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('min')">
63   - {{ 'device-profile.condition-duration-value-range' | translate }}
64   - </mat-error>
65   - <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('max')">
66   - {{ 'device-profile.condition-duration-value-range' | translate }}
67   - </mat-error>
68   - <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('pattern')">
69   - {{ 'device-profile.condition-duration-value-pattern' | translate }}
70   - </mat-error>
71   - </mat-form-field>
72   - <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
73   - <mat-label></mat-label>
74   - <mat-select formControlName="unit"
75   - required
76   - placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
77   - <mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
78   - {{ timeUnitTranslations.get(timeUnit) | translate }}
79   - </mat-option>
80   - </mat-select>
81   - <mat-error *ngIf="conditionFormGroup.get('spec.unit').hasError('required')">
82   - {{ 'device-profile.condition-duration-time-unit-required' | translate }}
83   - </mat-error>
84   - </mat-form-field>
85   - </div>
86   - <div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.REPEATING">
87   - <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
88   - <mat-label></mat-label>
89   - <input type="number" required
90   - step="1" min="1" max="2147483647" matInput
91   - placeholder="{{ 'device-profile.condition-repeating-value' | translate }}"
92   - formControlName="count">
93   - <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('required')">
94   - {{ 'device-profile.condition-repeating-value-required' | translate }}
95   - </mat-error>
96   - <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('min')">
97   - {{ 'device-profile.condition-repeating-value-range' | translate }}
98   - </mat-error>
99   - <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('max')">
100   - {{ 'device-profile.condition-repeating-value-range' | translate }}
101   - </mat-error>
102   - <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('pattern')">
103   - {{ 'device-profile.condition-repeating-value-pattern' | translate }}
104   - </mat-error>
105   - </mat-form-field>
  52 + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px" *ngIf="conditionFormGroup.get('spec.type').value != AlarmConditionType.SIMPLE">
  53 + <tb-alarm-duration-predicate-value
  54 + fxLayout="row" fxFlex formControlName="predicate"
  55 + [alarmConditionType]="conditionFormGroup.get('spec.type').value"
  56 + >
  57 + </tb-alarm-duration-predicate-value>
  58 + <div fxFlex="23" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.DURATION">
  59 + <mat-form-field class="mat-block" hideRequiredMarker floatLabel="always">
  60 + <mat-label></mat-label>
  61 + <mat-select formControlName="unit" required
  62 + placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
  63 + <mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
  64 + {{ timeUnitTranslations.get(timeUnit) | translate }}
  65 + </mat-option>
  66 + </mat-select>
  67 + <mat-error *ngIf="conditionFormGroup.get('spec.unit').hasError('required')">
  68 + {{ 'device-profile.condition-duration-time-unit-required' | translate }}
  69 + </mat-error>
  70 + </mat-form-field>
  71 + </div>
106 72 </div>
107 73 </section>
108 74 </div>
... ...
... ... @@ -43,12 +43,11 @@ export interface AlarmRuleConditionDialogData {
43 43 export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRuleConditionDialogComponent, AlarmCondition>
44 44 implements OnInit, ErrorStateMatcher {
45 45
46   - timeUnits = Object.keys(TimeUnit);
  46 + timeUnits = Object.values(TimeUnit);
47 47 timeUnitTranslations = timeUnitTranslationMap;
48   - alarmConditionTypes = Object.keys(AlarmConditionType);
  48 + alarmConditionTypes = Object.values(AlarmConditionType);
49 49 AlarmConditionType = AlarmConditionType;
50 50 alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap;
51   -
52 51 readonly = this.data.readonly;
53 52 condition = this.data.condition;
54 53 entityId = this.data.entityId;
... ... @@ -70,9 +69,8 @@ export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRule
70 69 keyFilters: [keyFiltersToKeyFilterInfos(this.condition?.condition), Validators.required],
71 70 spec: this.fb.group({
72 71 type: [AlarmConditionType.SIMPLE, Validators.required],
73   - unit: [{value: null, disable: true}, Validators.required],
74   - value: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]],
75   - count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]]
  72 + unit: [null, Validators.required],
  73 + predicate: [null, Validators.required]
76 74 })
77 75 });
78 76 this.conditionFormGroup.patchValue({spec: this.condition?.spec});
... ... @@ -98,42 +96,37 @@ export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRule
98 96 private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) {
99 97 switch (type) {
100 98 case AlarmConditionType.DURATION:
101   - this.conditionFormGroup.get('spec.value').enable();
102 99 this.conditionFormGroup.get('spec.unit').enable();
103   - this.conditionFormGroup.get('spec.count').disable();
  100 + this.conditionFormGroup.get('spec.predicate').enable();
104 101 if (resetDuration) {
105 102 this.conditionFormGroup.get('spec').patchValue({
106   - count: null
  103 + predicate: null
107 104 });
108 105 }
109 106 break;
110 107 case AlarmConditionType.REPEATING:
111   - this.conditionFormGroup.get('spec.count').enable();
112   - this.conditionFormGroup.get('spec.value').disable();
  108 + this.conditionFormGroup.get('spec.predicate').enable();
113 109 this.conditionFormGroup.get('spec.unit').disable();
114 110 if (resetDuration) {
115 111 this.conditionFormGroup.get('spec').patchValue({
116   - value: null,
117   - unit: null
  112 + unit: null,
  113 + predicate: null
118 114 });
119 115 }
120 116 break;
121 117 case AlarmConditionType.SIMPLE:
122   - this.conditionFormGroup.get('spec.value').disable();
123 118 this.conditionFormGroup.get('spec.unit').disable();
124   - this.conditionFormGroup.get('spec.count').disable();
  119 + this.conditionFormGroup.get('spec.predicate').disable();
125 120 if (resetDuration) {
126 121 this.conditionFormGroup.get('spec').patchValue({
127   - value: null,
128 122 unit: null,
129   - count: null
  123 + predicate: null
130 124 });
131 125 }
132 126 break;
133 127 }
134   - this.conditionFormGroup.get('spec.value').updateValueAndValidity({emitEvent});
  128 + this.conditionFormGroup.get('spec.predicate').updateValueAndValidity({emitEvent});
135 129 this.conditionFormGroup.get('spec.unit').updateValueAndValidity({emitEvent});
136   - this.conditionFormGroup.get('spec.count').updateValueAndValidity({emitEvent});
137 130 }
138 131
139 132 cancel(): void {
... ...
... ... @@ -18,22 +18,25 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core';
18 18 import {
19 19 ControlValueAccessor,
20 20 FormBuilder,
21   - FormControl, FormGroup,
  21 + FormControl,
  22 + FormGroup,
22 23 NG_VALIDATORS,
23 24 NG_VALUE_ACCESSOR,
24   - Validator, Validators
  25 + Validator,
  26 + Validators
25 27 } from '@angular/forms';
26 28 import { MatDialog } from '@angular/material/dialog';
27 29 import { deepClone, isUndefined } from '@core/utils';
28 30 import { TranslateService } from '@ngx-translate/core';
29 31 import { DatePipe } from '@angular/common';
30   -import { AlarmCondition, AlarmConditionSpec, AlarmConditionType } from '@shared/models/device.models';
  32 +import { AlarmCondition, AlarmConditionType } from '@shared/models/device.models';
31 33 import {
32 34 AlarmRuleConditionDialogComponent,
33 35 AlarmRuleConditionDialogData
34 36 } from '@home/components/profile/alarm/alarm-rule-condition-dialog.component';
35 37 import { TimeUnit } from '@shared/models/time/time.models';
36 38 import { EntityId } from '@shared/models/id/entity-id';
  39 +import { dynamicValueSourceTypeTranslationMap } from '@shared/models/query/query.models';
37 40
38 41 @Component({
39 42 selector: 'tb-alarm-rule-condition',
... ... @@ -159,22 +162,43 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
159 162 let duringText = '';
160 163 switch (spec.unit) {
161 164 case TimeUnit.SECONDS:
162   - duringText = this.translate.instant('timewindow.seconds', {seconds: spec.value});
  165 + duringText = this.translate.instant('timewindow.seconds', {seconds: spec.predicate.defaultValue});
163 166 break;
164 167 case TimeUnit.MINUTES:
165   - duringText = this.translate.instant('timewindow.minutes', {minutes: spec.value});
  168 + duringText = this.translate.instant('timewindow.minutes', {minutes: spec.predicate.defaultValue});
166 169 break;
167 170 case TimeUnit.HOURS:
168   - duringText = this.translate.instant('timewindow.hours', {hours: spec.value});
  171 + duringText = this.translate.instant('timewindow.hours', {hours: spec.predicate.defaultValue});
169 172 break;
170 173 case TimeUnit.DAYS:
171   - duringText = this.translate.instant('timewindow.days', {days: spec.value});
  174 + duringText = this.translate.instant('timewindow.days', {days: spec.predicate.defaultValue});
172 175 break;
173 176 }
174   - this.specText = this.translate.instant('device-profile.condition-during', {during: duringText});
  177 + if (spec.predicate.dynamicValue && spec.predicate.dynamicValue.sourceAttribute) {
  178 + const attributeSource =
  179 + this.translate.instant(dynamicValueSourceTypeTranslationMap.get(spec.predicate.dynamicValue.sourceType));
  180 + this.specText = this.translate.instant('device-profile.condition-during-dynamic', {
  181 + during: duringText,
  182 + attribute: `${attributeSource}.${spec.predicate.dynamicValue.sourceAttribute}`
  183 + });
  184 + } else {
  185 + this.specText = this.translate.instant('device-profile.condition-during', {
  186 + during: duringText
  187 + });
  188 + }
175 189 break;
176 190 case AlarmConditionType.REPEATING:
177   - this.specText = this.translate.instant('device-profile.condition-repeat-times', {count: spec.count});
  191 + if (spec.predicate.dynamicValue && spec.predicate.dynamicValue.sourceAttribute) {
  192 + const attributeSource =
  193 + this.translate.instant(dynamicValueSourceTypeTranslationMap.get(spec.predicate.dynamicValue.sourceType));
  194 + this.specText = this.translate.instant('device-profile.condition-repeat-times-dynamic', {
  195 + count: spec.predicate.defaultValue,
  196 + attribute: `${attributeSource}.${spec.predicate.dynamicValue.sourceAttribute}`
  197 + });
  198 + } else {
  199 + this.specText = this.translate.instant('device-profile.condition-repeat-times',
  200 + {count: spec.predicate.defaultValue});
  201 + }
178 202 break;
179 203 }
180 204 }
... ...
... ... @@ -15,7 +15,8 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<div class="tb-request-password-reset-content mat-app-background tb-dark" fxLayout="row" fxLayoutAlign="center center" style="width: 100%;">
  18 +<div class="tb-request-password-reset-content mat-app-background tb-dark" fxLayout="row" fxLayoutAlign="center center"
  19 + style="width: 100%;">
19 20 <mat-card fxFlex="initial" class="tb-request-password-reset-card">
20 21 <mat-card-title class="layout-padding">
21 22 <span translate class="mat-headline">login.request-password-reset</span>
... ... @@ -38,7 +39,7 @@
38 39 </mat-form-field>
39 40 <div fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="16px" fxLayoutAlign="start center"
40 41 fxLayoutAlign.gt-xs="center start">
41   - <button mat-raised-button color="accent" type="submit" [disabled]="(isLoading$ | async)">
  42 + <button mat-raised-button color="accent" type="submit" [disabled]="(isLoading$ | async) || this.clicked">
42 43 {{ 'login.request-password-reset' | translate }}
43 44 </button>
44 45 <button mat-raised-button color="primary" type="button" [disabled]="(isLoading$ | async)"
... ...
... ... @@ -30,6 +30,8 @@ import { TranslateService } from '@ngx-translate/core';
30 30 })
31 31 export class ResetPasswordRequestComponent extends PageComponent implements OnInit {
32 32
  33 + clicked: boolean = false;
  34 +
33 35 requestPasswordRequest = this.fb.group({
34 36 email: ['', [Validators.email, Validators.required]]
35 37 }, {updateOn: 'submit'});
... ... @@ -44,8 +46,14 @@ export class ResetPasswordRequestComponent extends PageComponent implements OnIn
44 46 ngOnInit() {
45 47 }
46 48
  49 + disableInputs() {
  50 + this.requestPasswordRequest.disable();
  51 + this.clicked = true;
  52 + }
  53 +
47 54 sendResetPasswordLink() {
48 55 if (this.requestPasswordRequest.valid) {
  56 + this.disableInputs();
49 57 this.authService.sendResetPasswordLink(this.requestPasswordRequest.get('email').value).subscribe(
50 58 () => {
51 59 this.store.dispatch(new ActionNotificationShow({
... ...
... ... @@ -23,7 +23,7 @@ import { EntitySearchQuery } from '@shared/models/relation.models';
23 23 import { DeviceProfileId } from '@shared/models/id/device-profile-id';
24 24 import { RuleChainId } from '@shared/models/id/rule-chain-id';
25 25 import { EntityInfoData } from '@shared/models/entity.models';
26   -import { KeyFilter } from '@shared/models/query/query.models';
  26 +import { FilterPredicateValue, KeyFilter } from '@shared/models/query/query.models';
27 27 import { TimeUnit } from '@shared/models/time/time.models';
28 28 import * as _moment from 'moment';
29 29 import { AbstractControl, ValidationErrors } from '@angular/forms';
... ... @@ -424,8 +424,7 @@ export const AlarmConditionTypeTranslationMap = new Map<AlarmConditionType, stri
424 424 export interface AlarmConditionSpec{
425 425 type?: AlarmConditionType;
426 426 unit?: TimeUnit;
427   - value?: number;
428   - count?: number;
  427 + predicate: FilterPredicateValue<number>;
429 428 }
430 429
431 430 export interface AlarmCondition {
... ...
... ... @@ -202,6 +202,17 @@ export function createDefaultFilterPredicate(valueType: EntityKeyValueType, comp
202 202 return predicate;
203 203 }
204 204
  205 +export function getDynamicSourcesForAllowUser(allow: boolean): DynamicValueSourceType[] {
  206 + const dynamicValueSourceTypes = [DynamicValueSourceType.CURRENT_TENANT,
  207 + DynamicValueSourceType.CURRENT_CUSTOMER];
  208 + if (allow) {
  209 + dynamicValueSourceTypes.push(DynamicValueSourceType.CURRENT_USER);
  210 + } else {
  211 + dynamicValueSourceTypes.push(DynamicValueSourceType.CURRENT_DEVICE);
  212 + }
  213 + return dynamicValueSourceTypes;
  214 +}
  215 +
205 216 export enum FilterPredicateType {
206 217 STRING = 'STRING',
207 218 NUMERIC = 'NUMERIC',
... ... @@ -289,6 +300,10 @@ export const dynamicValueSourceTypeTranslationMap = new Map<DynamicValueSourceTy
289 300 ]
290 301 );
291 302
  303 +export const inheritModeForDynamicValueSourceType = [
  304 + DynamicValueSourceType.CURRENT_CUSTOMER,
  305 + DynamicValueSourceType.CURRENT_DEVICE];
  306 +
292 307 export interface DynamicValue<T> {
293 308 sourceType: DynamicValueSourceType;
294 309 sourceAttribute: string;
... ...
... ... @@ -1178,6 +1178,7 @@
1178 1178 "condition-type-simple": "Simple",
1179 1179 "condition-type-duration": "Duration",
1180 1180 "condition-during": "During {{during}}",
  1181 + "condition-during-dynamic": "During \"{{ attribute }}\" ({{during}})",
1181 1182 "condition-type-repeating": "Repeating",
1182 1183 "condition-type-required": "Condition type is required.",
1183 1184 "condition-repeating-value": "Count of events",
... ... @@ -1185,6 +1186,7 @@
1185 1186 "condition-repeating-value-pattern": "Count of events should be integers.",
1186 1187 "condition-repeating-value-required": "Count of events is required.",
1187 1188 "condition-repeat-times": "Repeats { count, plural, 1 {1 time} other {# times} }",
  1189 + "condition-repeat-times-dynamic": "Repeats \"{ attribute }\" ({ count, plural, 1 {1 time} other {# times} })",
1188 1190 "schedule-type": "Scheduler type",
1189 1191 "schedule-type-required": "Scheduler type is required.",
1190 1192 "schedule": "Schedule",
... ... @@ -2268,7 +2270,7 @@
2268 2270 "expired-password-reset-message": "Your credentials has been expired! Please create new password.",
2269 2271 "new-password": "New password",
2270 2272 "new-password-again": "New password again",
2271   - "password-link-sent-message": "Password reset link was successfully sent!",
  2273 + "password-link-sent-message": "Reset link has been sent",
2272 2274 "email": "Email",
2273 2275 "login-with": "Login with {{name}}",
2274 2276 "or": "or",
... ...