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,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 public ResponseEntity<String> checkActivateToken( 139 public ResponseEntity<String> checkActivateToken(
140 @RequestParam(value = "activateToken") String activateToken) { 140 @RequestParam(value = "activateToken") String activateToken) {
141 HttpHeaders headers = new HttpHeaders(); 141 HttpHeaders headers = new HttpHeaders();
@@ -159,7 +159,7 @@ public class AuthController extends BaseController { @@ -159,7 +159,7 @@ public class AuthController extends BaseController {
159 159
160 @RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST) 160 @RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST)
161 @ResponseStatus(value = HttpStatus.OK) 161 @ResponseStatus(value = HttpStatus.OK)
162 - public void requestResetPasswordByEmail ( 162 + public void requestResetPasswordByEmail(
163 @RequestBody JsonNode resetPasswordByEmailRequest, 163 @RequestBody JsonNode resetPasswordByEmailRequest,
164 HttpServletRequest request) throws ThingsboardException { 164 HttpServletRequest request) throws ThingsboardException {
165 try { 165 try {
@@ -170,13 +170,13 @@ public class AuthController extends BaseController { @@ -170,13 +170,13 @@ public class AuthController extends BaseController {
170 String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl, 170 String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
171 userCredentials.getResetToken()); 171 userCredentials.getResetToken());
172 172
173 - mailService.sendResetPasswordEmail(resetUrl, email); 173 + mailService.sendResetPasswordEmailAsync(resetUrl, email);
174 } catch (Exception e) { 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 public ResponseEntity<String> checkResetToken( 180 public ResponseEntity<String> checkResetToken(
181 @RequestParam(value = "resetToken") String resetToken) { 181 @RequestParam(value = "resetToken") String resetToken) {
182 HttpHeaders headers = new HttpHeaders(); 182 HttpHeaders headers = new HttpHeaders();
@@ -25,6 +25,7 @@ import org.springframework.security.authentication.BadCredentialsException; @@ -25,6 +25,7 @@ import org.springframework.security.authentication.BadCredentialsException;
25 import org.springframework.security.authentication.DisabledException; 25 import org.springframework.security.authentication.DisabledException;
26 import org.springframework.security.authentication.LockedException; 26 import org.springframework.security.authentication.LockedException;
27 import org.springframework.security.core.AuthenticationException; 27 import org.springframework.security.core.AuthenticationException;
  28 +import org.springframework.security.core.userdetails.UsernameNotFoundException;
28 import org.springframework.security.web.access.AccessDeniedHandler; 29 import org.springframework.security.web.access.AccessDeniedHandler;
29 import org.springframework.web.bind.annotation.ExceptionHandler; 30 import org.springframework.web.bind.annotation.ExceptionHandler;
30 import org.springframework.web.bind.annotation.RestControllerAdvice; 31 import org.springframework.web.bind.annotation.RestControllerAdvice;
@@ -152,7 +153,7 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand @@ -152,7 +153,7 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
152 153
153 private void handleAuthenticationException(AuthenticationException authenticationException, HttpServletResponse response) throws IOException { 154 private void handleAuthenticationException(AuthenticationException authenticationException, HttpServletResponse response) throws IOException {
154 response.setStatus(HttpStatus.UNAUTHORIZED.value()); 155 response.setStatus(HttpStatus.UNAUTHORIZED.value());
155 - if (authenticationException instanceof BadCredentialsException) { 156 + if (authenticationException instanceof BadCredentialsException || authenticationException instanceof UsernameNotFoundException) {
156 mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); 157 mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
157 } else if (authenticationException instanceof DisabledException) { 158 } else if (authenticationException instanceof DisabledException) {
158 mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); 159 mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 */ 15 */
16 package org.thingsboard.server.service.install.update; 16 package org.thingsboard.server.service.install.update;
17 17
  18 +import com.fasterxml.jackson.databind.JsonNode;
18 import com.fasterxml.jackson.databind.node.ObjectNode; 19 import com.fasterxml.jackson.databind.node.ObjectNode;
19 import com.google.common.util.concurrent.Futures; 20 import com.google.common.util.concurrent.Futures;
20 import com.google.common.util.concurrent.ListenableFuture; 21 import com.google.common.util.concurrent.ListenableFuture;
@@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.Tenant; @@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.Tenant;
31 import org.thingsboard.server.common.data.alarm.Alarm; 32 import org.thingsboard.server.common.data.alarm.Alarm;
32 import org.thingsboard.server.common.data.alarm.AlarmInfo; 33 import org.thingsboard.server.common.data.alarm.AlarmInfo;
33 import org.thingsboard.server.common.data.alarm.AlarmQuery; 34 import org.thingsboard.server.common.data.alarm.AlarmQuery;
  35 +import org.thingsboard.server.common.data.alarm.AlarmSeverity;
34 import org.thingsboard.server.common.data.id.EntityViewId; 36 import org.thingsboard.server.common.data.id.EntityViewId;
35 import org.thingsboard.server.common.data.id.TenantId; 37 import org.thingsboard.server.common.data.id.TenantId;
36 import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; 38 import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
@@ -41,16 +43,21 @@ import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientsParams; @@ -41,16 +43,21 @@ import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientsParams;
41 import org.thingsboard.server.common.data.page.PageData; 43 import org.thingsboard.server.common.data.page.PageData;
42 import org.thingsboard.server.common.data.page.PageLink; 44 import org.thingsboard.server.common.data.page.PageLink;
43 import org.thingsboard.server.common.data.page.TimePageLink; 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 import org.thingsboard.server.common.data.rule.RuleChain; 48 import org.thingsboard.server.common.data.rule.RuleChain;
45 import org.thingsboard.server.common.data.rule.RuleChainMetaData; 49 import org.thingsboard.server.common.data.rule.RuleChainMetaData;
46 import org.thingsboard.server.common.data.rule.RuleNode; 50 import org.thingsboard.server.common.data.rule.RuleNode;
  51 +import org.thingsboard.server.dao.DaoUtil;
47 import org.thingsboard.server.dao.alarm.AlarmDao; 52 import org.thingsboard.server.dao.alarm.AlarmDao;
48 import org.thingsboard.server.dao.alarm.AlarmService; 53 import org.thingsboard.server.dao.alarm.AlarmService;
49 import org.thingsboard.server.dao.entity.EntityService; 54 import org.thingsboard.server.dao.entity.EntityService;
50 import org.thingsboard.server.dao.entityview.EntityViewService; 55 import org.thingsboard.server.dao.entityview.EntityViewService;
  56 +import org.thingsboard.server.dao.model.sql.DeviceProfileEntity;
51 import org.thingsboard.server.dao.oauth2.OAuth2Service; 57 import org.thingsboard.server.dao.oauth2.OAuth2Service;
52 import org.thingsboard.server.dao.oauth2.OAuth2Utils; 58 import org.thingsboard.server.dao.oauth2.OAuth2Utils;
53 import org.thingsboard.server.dao.rule.RuleChainService; 59 import org.thingsboard.server.dao.rule.RuleChainService;
  60 +import org.thingsboard.server.dao.sql.device.DeviceProfileRepository;
54 import org.thingsboard.server.dao.tenant.TenantService; 61 import org.thingsboard.server.dao.tenant.TenantService;
55 import org.thingsboard.server.dao.timeseries.TimeseriesService; 62 import org.thingsboard.server.dao.timeseries.TimeseriesService;
56 import org.thingsboard.server.service.install.InstallScripts; 63 import org.thingsboard.server.service.install.InstallScripts;
@@ -93,6 +100,9 @@ public class DefaultDataUpdateService implements DataUpdateService { @@ -93,6 +100,9 @@ public class DefaultDataUpdateService implements DataUpdateService {
93 private AlarmDao alarmDao; 100 private AlarmDao alarmDao;
94 101
95 @Autowired 102 @Autowired
  103 + private DeviceProfileRepository deviceProfileRepository;
  104 +
  105 + @Autowired
96 private OAuth2Service oAuth2Service; 106 private OAuth2Service oAuth2Service;
97 107
98 @Override 108 @Override
@@ -114,6 +124,7 @@ public class DefaultDataUpdateService implements DataUpdateService { @@ -114,6 +124,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
114 log.info("Updating data from version 3.2.2 to 3.3.0 ..."); 124 log.info("Updating data from version 3.2.2 to 3.3.0 ...");
115 tenantsDefaultEdgeRuleChainUpdater.updateEntities(null); 125 tenantsDefaultEdgeRuleChainUpdater.updateEntities(null);
116 tenantsAlarmsCustomerUpdater.updateEntities(null); 126 tenantsAlarmsCustomerUpdater.updateEntities(null);
  127 + deviceProfileEntityDynamicConditionsUpdater.updateEntities(null);
117 updateOAuth2Params(); 128 updateOAuth2Params();
118 break; 129 break;
119 default: 130 default:
@@ -121,6 +132,45 @@ public class DefaultDataUpdateService implements DataUpdateService { @@ -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 private final PaginatedUpdater<String, Tenant> tenantsDefaultRuleChainUpdater = 174 private final PaginatedUpdater<String, Tenant> tenantsDefaultRuleChainUpdater =
125 new PaginatedUpdater<>() { 175 new PaginatedUpdater<>() {
126 176
@@ -370,6 +420,33 @@ public class DefaultDataUpdateService implements DataUpdateService { @@ -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 private void updateOAuth2Params() { 450 private void updateOAuth2Params() {
374 try { 451 try {
375 OAuth2ClientsParams oauth2ClientsParams = oAuth2Service.findOAuth2Params(); 452 OAuth2ClientsParams oauth2ClientsParams = oAuth2Service.findOAuth2Params();
@@ -380,9 +457,8 @@ public class DefaultDataUpdateService implements DataUpdateService { @@ -380,9 +457,8 @@ public class DefaultDataUpdateService implements DataUpdateService {
380 oAuth2Service.saveOAuth2Params(new OAuth2ClientsParams(false, Collections.emptyList())); 457 oAuth2Service.saveOAuth2Params(new OAuth2ClientsParams(false, Collections.emptyList()));
381 log.info("Successfully updated OAuth2 parameters!"); 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,7 +22,7 @@ import org.thingsboard.server.common.data.page.PageData;
22 import org.thingsboard.server.common.data.page.PageLink; 22 import org.thingsboard.server.common.data.page.PageLink;
23 23
24 @Slf4j 24 @Slf4j
25 -public abstract class PaginatedUpdater<I, D extends SearchTextBased<? extends UUIDBased>> { 25 +public abstract class PaginatedUpdater<I, D> {
26 26
27 private static final int DEFAULT_LIMIT = 100; 27 private static final int DEFAULT_LIMIT = 100;
28 private int updated = 0; 28 private int updated = 0;
@@ -73,6 +73,9 @@ public class DefaultMailService implements MailService { @@ -73,6 +73,9 @@ public class DefaultMailService implements MailService {
73 @Autowired 73 @Autowired
74 private TbApiUsageStateService apiUsageStateService; 74 private TbApiUsageStateService apiUsageStateService;
75 75
  76 + @Autowired
  77 + private MailExecutorService mailExecutorService;
  78 +
76 private JavaMailSenderImpl mailSender; 79 private JavaMailSenderImpl mailSender;
77 80
78 private String mailFrom; 81 private String mailFrom;
@@ -222,6 +225,17 @@ public class DefaultMailService implements MailService { @@ -222,6 +225,17 @@ public class DefaultMailService implements MailService {
222 } 225 }
223 226
224 @Override 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 public void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException { 239 public void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException {
226 240
227 String subject = messages.getMessage("password.was.reset.subject", null, Locale.US); 241 String subject = messages.getMessage("password.was.reset.subject", null, Locale.US);
@@ -206,11 +206,7 @@ public abstract class AbstractOAuth2ClientMapper { @@ -206,11 +206,7 @@ public abstract class AbstractOAuth2ClientMapper {
206 } 206 }
207 207
208 private Optional<DashboardId> getDashboardId(TenantId tenantId, String dashboardName) { 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 private Optional<DashboardId> getDashboardId(TenantId tenantId, CustomerId customerId, String dashboardName) { 212 private Optional<DashboardId> getDashboardId(TenantId tenantId, CustomerId customerId, String dashboardName) {
@@ -155,6 +155,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { @@ -155,6 +155,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
155 155
156 doPost("/api/noauth/resetPasswordByEmail", resetPasswordByEmailRequest) 156 doPost("/api/noauth/resetPasswordByEmail", resetPasswordByEmailRequest)
157 .andExpect(status().isOk()); 157 .andExpect(status().isOk());
  158 + Thread.sleep(1000);
158 doGet("/api/noauth/resetPassword?resetToken={resetToken}", TestMailService.currentResetPasswordToken) 159 doGet("/api/noauth/resetPassword?resetToken={resetToken}", TestMailService.currentResetPasswordToken)
159 .andExpect(status().isSeeOther()) 160 .andExpect(status().isSeeOther())
160 .andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + TestMailService.currentResetPasswordToken)); 161 .andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + TestMailService.currentResetPasswordToken));
@@ -51,7 +51,7 @@ public class TestMailService { @@ -51,7 +51,7 @@ public class TestMailService {
51 currentResetPasswordToken = passwordResetLink.split("=")[1]; 51 currentResetPasswordToken = passwordResetLink.split("=")[1];
52 return null; 52 return null;
53 } 53 }
54 - }).when(mailService).sendResetPasswordEmail(Mockito.anyString(), Mockito.anyString()); 54 + }).when(mailService).sendResetPasswordEmailAsync(Mockito.anyString(), Mockito.anyString());
55 return mailService; 55 return mailService;
56 } 56 }
57 57
@@ -58,4 +58,6 @@ public interface DashboardService { @@ -58,4 +58,6 @@ public interface DashboardService {
58 Dashboard unassignDashboardFromEdge(TenantId tenantId, DashboardId dashboardId, EdgeId edgeId); 58 Dashboard unassignDashboardFromEdge(TenantId tenantId, DashboardId dashboardId, EdgeId edgeId);
59 59
60 PageData<DashboardInfo> findDashboardsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink); 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,6 +17,7 @@ package org.thingsboard.server.common.data.device.profile;
17 17
18 import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 18 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
19 import lombok.Data; 19 import lombok.Data;
  20 +import org.thingsboard.server.common.data.query.FilterPredicateValue;
20 21
21 import java.util.concurrent.TimeUnit; 22 import java.util.concurrent.TimeUnit;
22 23
@@ -25,7 +26,7 @@ import java.util.concurrent.TimeUnit; @@ -25,7 +26,7 @@ import java.util.concurrent.TimeUnit;
25 public class DurationAlarmConditionSpec implements AlarmConditionSpec { 26 public class DurationAlarmConditionSpec implements AlarmConditionSpec {
26 27
27 private TimeUnit unit; 28 private TimeUnit unit;
28 - private long value; 29 + private FilterPredicateValue<Long> predicate;
29 30
30 @Override 31 @Override
31 public AlarmConditionSpecType getType() { 32 public AlarmConditionSpecType getType() {
@@ -17,14 +17,13 @@ package org.thingsboard.server.common.data.device.profile; @@ -17,14 +17,13 @@ package org.thingsboard.server.common.data.device.profile;
17 17
18 import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 18 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
19 import lombok.Data; 19 import lombok.Data;
20 -  
21 -import java.util.concurrent.TimeUnit; 20 +import org.thingsboard.server.common.data.query.FilterPredicateValue;
22 21
23 @Data 22 @Data
24 @JsonIgnoreProperties(ignoreUnknown = true) 23 @JsonIgnoreProperties(ignoreUnknown = true)
25 public class RepeatingAlarmConditionSpec implements AlarmConditionSpec { 24 public class RepeatingAlarmConditionSpec implements AlarmConditionSpec {
26 25
27 - private int count; 26 + private FilterPredicateValue<Integer> predicate;
28 27
29 @Override 28 @Override
30 public AlarmConditionSpecType getType() { 29 public AlarmConditionSpecType getType() {
@@ -56,4 +56,6 @@ public interface DashboardInfoDao extends Dao<DashboardInfo> { @@ -56,4 +56,6 @@ public interface DashboardInfoDao extends Dao<DashboardInfo> {
56 */ 56 */
57 PageData<DashboardInfo> findDashboardsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink); 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,7 +34,6 @@ import org.thingsboard.server.common.data.id.EdgeId;
34 import org.thingsboard.server.common.data.id.TenantId; 34 import org.thingsboard.server.common.data.id.TenantId;
35 import org.thingsboard.server.common.data.page.PageData; 35 import org.thingsboard.server.common.data.page.PageData;
36 import org.thingsboard.server.common.data.page.PageLink; 36 import org.thingsboard.server.common.data.page.PageLink;
37 -import org.thingsboard.server.common.data.page.TimePageLink;  
38 import org.thingsboard.server.common.data.relation.EntityRelation; 37 import org.thingsboard.server.common.data.relation.EntityRelation;
39 import org.thingsboard.server.common.data.relation.RelationTypeGroup; 38 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
40 import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; 39 import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
@@ -269,6 +268,11 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @@ -269,6 +268,11 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
269 return dashboardInfoDao.findDashboardsByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); 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 private DataValidator<Dashboard> dashboardValidator = 276 private DataValidator<Dashboard> dashboardValidator =
273 new DataValidator<Dashboard>() { 277 new DataValidator<Dashboard>() {
274 @Override 278 @Override
@@ -29,6 +29,8 @@ import java.util.UUID; @@ -29,6 +29,8 @@ import java.util.UUID;
29 */ 29 */
30 public interface DashboardInfoRepository extends PagingAndSortingRepository<DashboardInfoEntity, UUID> { 30 public interface DashboardInfoRepository extends PagingAndSortingRepository<DashboardInfoEntity, UUID> {
31 31
  32 + DashboardInfoEntity findFirstByTenantIdAndTitle(UUID tenantId, String title);
  33 +
32 @Query("SELECT di FROM DashboardInfoEntity di WHERE di.tenantId = :tenantId " + 34 @Query("SELECT di FROM DashboardInfoEntity di WHERE di.tenantId = :tenantId " +
33 "AND LOWER(di.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") 35 "AND LOWER(di.searchText) LIKE LOWER(CONCAT(:searchText, '%'))")
34 Page<DashboardInfoEntity> findByTenantId(@Param("tenantId") UUID tenantId, 36 Page<DashboardInfoEntity> findByTenantId(@Param("tenantId") UUID tenantId,
@@ -83,4 +83,9 @@ public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao<DashboardInfoE @@ -83,4 +83,9 @@ public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao<DashboardInfoE
83 Objects.toString(pageLink.getTextSearch(), ""), 83 Objects.toString(pageLink.getTextSearch(), ""),
84 DaoUtil.toPageable(pageLink))); 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,7 +25,10 @@ import org.apache.commons.lang3.StringUtils;
25 import org.springframework.beans.factory.annotation.Value; 25 import org.springframework.beans.factory.annotation.Value;
26 import org.springframework.context.ApplicationEventPublisher; 26 import org.springframework.context.ApplicationEventPublisher;
27 import org.springframework.context.annotation.Lazy; 27 import org.springframework.context.annotation.Lazy;
  28 +import org.springframework.security.authentication.DisabledException;
  29 +import org.springframework.security.core.userdetails.UsernameNotFoundException;
28 import org.springframework.stereotype.Service; 30 import org.springframework.stereotype.Service;
  31 +import org.thingsboard.common.util.JacksonUtil;
29 import org.thingsboard.server.common.data.Customer; 32 import org.thingsboard.server.common.data.Customer;
30 import org.thingsboard.server.common.data.EntityType; 33 import org.thingsboard.server.common.data.EntityType;
31 import org.thingsboard.server.common.data.Tenant; 34 import org.thingsboard.server.common.data.Tenant;
@@ -49,7 +52,6 @@ import org.thingsboard.server.dao.service.DataValidator; @@ -49,7 +52,6 @@ import org.thingsboard.server.dao.service.DataValidator;
49 import org.thingsboard.server.dao.service.PaginatedRemover; 52 import org.thingsboard.server.dao.service.PaginatedRemover;
50 import org.thingsboard.server.dao.tenant.TbTenantProfileCache; 53 import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
51 import org.thingsboard.server.dao.tenant.TenantDao; 54 import org.thingsboard.server.dao.tenant.TenantDao;
52 -import org.thingsboard.common.util.JacksonUtil;  
53 55
54 import java.util.HashMap; 56 import java.util.HashMap;
55 import java.util.Map; 57 import java.util.Map;
@@ -194,11 +196,11 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @@ -194,11 +196,11 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
194 DataValidator.validateEmail(email); 196 DataValidator.validateEmail(email);
195 User user = userDao.findByEmail(tenantId, email); 197 User user = userDao.findByEmail(tenantId, email);
196 if (user == null) { 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 UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, user.getUuidId()); 201 UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, user.getUuidId());
200 if (!userCredentials.isEnabled()) { 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 userCredentials.setResetToken(RandomStringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH)); 205 userCredentials.setResetToken(RandomStringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH));
204 return saveUserCredentials(tenantId, userCredentials); 206 return saveUserCredentials(tenantId, userCredentials);
@@ -365,7 +367,8 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @@ -365,7 +367,8 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
365 JsonNode userPasswordHistoryJson; 367 JsonNode userPasswordHistoryJson;
366 if (additionalInfo.has(USER_PASSWORD_HISTORY)) { 368 if (additionalInfo.has(USER_PASSWORD_HISTORY)) {
367 userPasswordHistoryJson = additionalInfo.get(USER_PASSWORD_HISTORY); 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 if (userPasswordHistoryMap != null) { 373 if (userPasswordHistoryMap != null) {
371 userPasswordHistoryMap.put(Long.toString(System.currentTimeMillis()), userCredentials.getPassword()); 374 userPasswordHistoryMap.put(Long.toString(System.currentTimeMillis()), userCredentials.getPassword());
@@ -31,22 +31,25 @@ public interface MailService { @@ -31,22 +31,25 @@ public interface MailService {
31 void updateMailConfiguration(); 31 void updateMailConfiguration();
32 32
33 void sendEmail(TenantId tenantId, String email, String subject, String message) throws ThingsboardException; 33 void sendEmail(TenantId tenantId, String email, String subject, String message) throws ThingsboardException;
34 - 34 +
35 void sendTestMail(JsonNode config, String email) throws ThingsboardException; 35 void sendTestMail(JsonNode config, String email) throws ThingsboardException;
36 - 36 +
37 void sendActivationEmail(String activationLink, String email) throws ThingsboardException; 37 void sendActivationEmail(String activationLink, String email) throws ThingsboardException;
38 - 38 +
39 void sendAccountActivatedEmail(String loginLink, String email) throws ThingsboardException; 39 void sendAccountActivatedEmail(String loginLink, String email) throws ThingsboardException;
40 - 40 +
41 void sendResetPasswordEmail(String passwordResetLink, String email) throws ThingsboardException; 41 void sendResetPasswordEmail(String passwordResetLink, String email) throws ThingsboardException;
42 42
  43 + void sendResetPasswordEmailAsync(String passwordResetLink, String email);
  44 +
43 void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException; 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 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; 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 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; 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 void sendApiFeatureStateEmail(ApiFeature apiFeature, ApiUsageStateValue stateValue, String email, ApiUsageStateMailMessage msg) throws ThingsboardException; 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,6 +24,7 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter;
24 import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; 24 import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey;
25 import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; 25 import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType;
26 import org.thingsboard.server.common.data.device.profile.AlarmConditionSpec; 26 import org.thingsboard.server.common.data.device.profile.AlarmConditionSpec;
  27 +import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType;
27 import org.thingsboard.server.common.data.device.profile.AlarmRule; 28 import org.thingsboard.server.common.data.device.profile.AlarmRule;
28 import org.thingsboard.server.common.data.device.profile.CustomTimeSchedule; 29 import org.thingsboard.server.common.data.device.profile.CustomTimeSchedule;
29 import org.thingsboard.server.common.data.device.profile.CustomTimeScheduleItem; 30 import org.thingsboard.server.common.data.device.profile.CustomTimeScheduleItem;
@@ -33,6 +34,7 @@ import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpe @@ -33,6 +34,7 @@ import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpe
33 import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule; 34 import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule;
34 import org.thingsboard.server.common.data.query.BooleanFilterPredicate; 35 import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
35 import org.thingsboard.server.common.data.query.ComplexFilterPredicate; 36 import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
  37 +import org.thingsboard.server.common.data.query.DynamicValue;
36 import org.thingsboard.server.common.data.query.FilterPredicateValue; 38 import org.thingsboard.server.common.data.query.FilterPredicateValue;
37 import org.thingsboard.server.common.data.query.KeyFilterPredicate; 39 import org.thingsboard.server.common.data.query.KeyFilterPredicate;
38 import org.thingsboard.server.common.data.query.NumericFilterPredicate; 40 import org.thingsboard.server.common.data.query.NumericFilterPredicate;
@@ -43,6 +45,7 @@ import java.time.Instant; @@ -43,6 +45,7 @@ import java.time.Instant;
43 import java.time.ZoneId; 45 import java.time.ZoneId;
44 import java.time.ZonedDateTime; 46 import java.time.ZonedDateTime;
45 import java.util.Set; 47 import java.util.Set;
  48 +import java.util.concurrent.TimeUnit;
46 import java.util.function.Function; 49 import java.util.function.Function;
47 50
48 @Data 51 @Data
@@ -52,8 +55,6 @@ class AlarmRuleState { @@ -52,8 +55,6 @@ class AlarmRuleState {
52 private final AlarmSeverity severity; 55 private final AlarmSeverity severity;
53 private final AlarmRule alarmRule; 56 private final AlarmRule alarmRule;
54 private final AlarmConditionSpec spec; 57 private final AlarmConditionSpec spec;
55 - private final long requiredDurationInMs;  
56 - private final long requiredRepeats;  
57 private final Set<AlarmConditionFilterKey> entityKeys; 58 private final Set<AlarmConditionFilterKey> entityKeys;
58 private PersistedAlarmRuleState state; 59 private PersistedAlarmRuleState state;
59 private boolean updateFlag; 60 private boolean updateFlag;
@@ -69,20 +70,6 @@ class AlarmRuleState { @@ -69,20 +70,6 @@ class AlarmRuleState {
69 this.state = new PersistedAlarmRuleState(0L, 0L, 0L); 70 this.state = new PersistedAlarmRuleState(0L, 0L, 0L);
70 } 71 }
71 this.spec = getSpec(alarmRule); 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 this.dynamicPredicateValueCtx = dynamicPredicateValueCtx; 73 this.dynamicPredicateValueCtx = dynamicPredicateValueCtx;
87 } 74 }
88 75
@@ -211,6 +198,7 @@ class AlarmRuleState { @@ -211,6 +198,7 @@ class AlarmRuleState {
211 if (active && eval(alarmRule.getCondition(), data)) { 198 if (active && eval(alarmRule.getCondition(), data)) {
212 state.setEventCount(state.getEventCount() + 1); 199 state.setEventCount(state.getEventCount() + 1);
213 updateFlag = true; 200 updateFlag = true;
  201 + long requiredRepeats = resolveRequiredRepeats(data);
214 return state.getEventCount() >= requiredRepeats ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE; 202 return state.getEventCount() >= requiredRepeats ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE;
215 } else { 203 } else {
216 return AlarmEvalResult.FALSE; 204 return AlarmEvalResult.FALSE;
@@ -230,18 +218,62 @@ class AlarmRuleState { @@ -230,18 +218,62 @@ class AlarmRuleState {
230 state.setDuration(0L); 218 state.setDuration(0L);
231 updateFlag = true; 219 updateFlag = true;
232 } 220 }
  221 + long requiredDurationInMs = resolveRequiredDurationInMs(data);
233 return state.getDuration() > requiredDurationInMs ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE; 222 return state.getDuration() > requiredDurationInMs ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE;
234 } else { 223 } else {
235 return AlarmEvalResult.FALSE; 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 switch (spec.getType()) { 271 switch (spec.getType()) {
241 case SIMPLE: 272 case SIMPLE:
242 case REPEATING: 273 case REPEATING:
243 return AlarmEvalResult.NOT_YET_TRUE; 274 return AlarmEvalResult.NOT_YET_TRUE;
244 case DURATION: 275 case DURATION:
  276 + long requiredDurationInMs = resolveRequiredDurationInMs(dataSnapshot);
245 if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && ts > state.getLastEventTs()) { 277 if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && ts > state.getLastEventTs()) {
246 long duration = state.getDuration() + (ts - state.getLastEventTs()); 278 long duration = state.getDuration() + (ts - state.getLastEventTs());
247 if (isActive(ts)) { 279 if (isActive(ts)) {
@@ -411,7 +443,7 @@ class AlarmRuleState { @@ -411,7 +443,7 @@ class AlarmRuleState {
411 } 443 }
412 444
413 private <T> T getPredicateValue(DataSnapshot data, FilterPredicateValue<T> value, AlarmConditionFilter filter, Function<EntityKeyValue, T> transformFunction) { 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 if (ekv != null) { 447 if (ekv != null) {
416 T result = transformFunction.apply(ekv); 448 T result = transformFunction.apply(ekv);
417 if (result != null) { 449 if (result != null) {
@@ -425,22 +457,22 @@ class AlarmRuleState { @@ -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 EntityKeyValue ekv = null; 461 EntityKeyValue ekv = null;
430 - if (value.getDynamicValue() != null) {  
431 - switch (value.getDynamicValue().getSourceType()) { 462 + if (value != null) {
  463 + switch (value.getSourceType()) {
432 case CURRENT_DEVICE: 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 break; 467 break;
436 } 468 }
437 case CURRENT_CUSTOMER: 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 break; 472 break;
441 } 473 }
442 case CURRENT_TENANT: 474 case CURRENT_TENANT:
443 - ekv = dynamicPredicateValueCtx.getTenantValue(value.getDynamicValue().getSourceAttribute()); 475 + ekv = dynamicPredicateValueCtx.getTenantValue(value.getSourceAttribute());
444 } 476 }
445 } 477 }
446 return ekv; 478 return ekv;
@@ -80,7 +80,7 @@ class AlarmState { @@ -80,7 +80,7 @@ class AlarmState {
80 80
81 public boolean process(TbContext ctx, long ts) throws ExecutionException, InterruptedException { 81 public boolean process(TbContext ctx, long ts) throws ExecutionException, InterruptedException {
82 initCurrentAlarm(ctx); 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 public <T> boolean createOrClearAlarms(TbContext ctx, TbMsg msg, T data, SnapshotUpdate update, BiFunction<AlarmRuleState, T, AlarmEvalResult> evalFunction) { 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,24 +19,21 @@ import lombok.AccessLevel;
19 import lombok.Getter; 19 import lombok.Getter;
20 import org.thingsboard.server.common.data.DeviceProfile; 20 import org.thingsboard.server.common.data.DeviceProfile;
21 import org.thingsboard.server.common.data.alarm.AlarmSeverity; 21 import org.thingsboard.server.common.data.alarm.AlarmSeverity;
22 -import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter;  
23 import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; 22 import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey;
24 import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; 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 import org.thingsboard.server.common.data.device.profile.AlarmRule; 26 import org.thingsboard.server.common.data.device.profile.AlarmRule;
26 import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; 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 import org.thingsboard.server.common.data.id.DeviceProfileId; 30 import org.thingsboard.server.common.data.id.DeviceProfileId;
28 import org.thingsboard.server.common.data.query.ComplexFilterPredicate; 31 import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
29 import org.thingsboard.server.common.data.query.DynamicValue; 32 import org.thingsboard.server.common.data.query.DynamicValue;
30 import org.thingsboard.server.common.data.query.DynamicValueSourceType; 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 import org.thingsboard.server.common.data.query.KeyFilterPredicate; 34 import org.thingsboard.server.common.data.query.KeyFilterPredicate;
36 import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate; 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 import java.util.Collections; 37 import java.util.Collections;
41 import java.util.HashMap; 38 import java.util.HashMap;
42 import java.util.HashSet; 39 import java.util.HashSet;
@@ -79,6 +76,7 @@ class ProfileState { @@ -79,6 +76,7 @@ class ProfileState {
79 ruleKeys.add(keyFilter.getKey()); 76 ruleKeys.add(keyFilter.getKey());
80 addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, ruleKeys); 77 addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, ruleKeys);
81 } 78 }
  79 + addEntityKeysFromAlarmConditionSpec(alarmRule);
82 })); 80 }));
83 if (alarm.getClearRule() != null) { 81 if (alarm.getClearRule() != null) {
84 var clearAlarmKeys = alarmClearKeys.computeIfAbsent(alarm.getId(), id -> new HashSet<>()); 82 var clearAlarmKeys = alarmClearKeys.computeIfAbsent(alarm.getId(), id -> new HashSet<>());
@@ -87,11 +85,43 @@ class ProfileState { @@ -87,11 +85,43 @@ class ProfileState {
87 clearAlarmKeys.add(keyFilter.getKey()); 85 clearAlarmKeys.add(keyFilter.getKey());
88 addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, clearAlarmKeys); 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 private void addDynamicValuesRecursively(KeyFilterPredicate predicate, Set<AlarmConditionFilterKey> entityKeys, Set<AlarmConditionFilterKey> ruleKeys) { 125 private void addDynamicValuesRecursively(KeyFilterPredicate predicate, Set<AlarmConditionFilterKey> entityKeys, Set<AlarmConditionFilterKey> ruleKeys) {
96 switch (predicate.getType()) { 126 switch (predicate.getType()) {
97 case STRING: 127 case STRING:
@@ -174,7 +174,13 @@ public class TbHttpClient { @@ -174,7 +174,13 @@ public class TbHttpClient {
174 String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg); 174 String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg);
175 HttpHeaders headers = prepareHeaders(msg); 175 HttpHeaders headers = prepareHeaders(msg);
176 HttpMethod method = HttpMethod.valueOf(config.getRequestMethod()); 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 ListenableFuture<ResponseEntity<String>> future = httpClient.exchange( 185 ListenableFuture<ResponseEntity<String>> future = httpClient.exchange(
180 endpointUrl, method, entity, String.class); 186 endpointUrl, method, entity, String.class);
@@ -42,6 +42,8 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; @@ -42,6 +42,8 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType;
42 import org.thingsboard.server.common.data.device.profile.AlarmRule; 42 import org.thingsboard.server.common.data.device.profile.AlarmRule;
43 import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; 43 import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
44 import org.thingsboard.server.common.data.device.profile.DeviceProfileData; 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 import org.thingsboard.server.common.data.id.CustomerId; 47 import org.thingsboard.server.common.data.id.CustomerId;
46 import org.thingsboard.server.common.data.id.DeviceId; 48 import org.thingsboard.server.common.data.id.DeviceId;
47 import org.thingsboard.server.common.data.id.DeviceProfileId; 49 import org.thingsboard.server.common.data.id.DeviceProfileId;
@@ -64,12 +66,15 @@ import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey; @@ -64,12 +66,15 @@ import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey;
64 import org.thingsboard.server.dao.model.sql.AttributeKvEntity; 66 import org.thingsboard.server.dao.model.sql.AttributeKvEntity;
65 import org.thingsboard.server.dao.timeseries.TimeseriesService; 67 import org.thingsboard.server.dao.timeseries.TimeseriesService;
66 68
  69 +import java.math.BigDecimal;
  70 +import java.math.RoundingMode;
67 import java.util.Arrays; 71 import java.util.Arrays;
68 import java.util.Collections; 72 import java.util.Collections;
69 import java.util.List; 73 import java.util.List;
70 import java.util.Optional; 74 import java.util.Optional;
71 import java.util.TreeMap; 75 import java.util.TreeMap;
72 import java.util.UUID; 76 import java.util.UUID;
  77 +import java.util.concurrent.TimeUnit;
73 78
74 import static org.mockito.ArgumentMatchers.eq; 79 import static org.mockito.ArgumentMatchers.eq;
75 import static org.mockito.Mockito.verify; 80 import static org.mockito.Mockito.verify;
@@ -94,10 +99,10 @@ public class TbDeviceProfileNodeTest { @@ -94,10 +99,10 @@ public class TbDeviceProfileNodeTest {
94 @Mock 99 @Mock
95 private AttributesService attributesService; 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 @Test 107 @Test
103 public void testRandomMessageType() throws Exception { 108 public void testRandomMessageType() throws Exception {
@@ -445,6 +450,642 @@ public class TbDeviceProfileNodeTest { @@ -445,6 +450,642 @@ public class TbDeviceProfileNodeTest {
445 } 450 }
446 451
447 @Test 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 public void testCurrentCustomersAttributeForDynamicValue() throws Exception { 1089 public void testCurrentCustomersAttributeForDynamicValue() throws Exception {
449 init(); 1090 init();
450 1091
@@ -30,7 +30,9 @@ import { @@ -30,7 +30,9 @@ import {
30 DynamicValueSourceType, 30 DynamicValueSourceType,
31 dynamicValueSourceTypeTranslationMap, 31 dynamicValueSourceTypeTranslationMap,
32 EntityKeyValueType, 32 EntityKeyValueType,
33 - FilterPredicateValue 33 + FilterPredicateValue,
  34 + getDynamicSourcesForAllowUser,
  35 + inheritModeForDynamicValueSourceType
34 } from '@shared/models/query/query.models'; 36 } from '@shared/models/query/query.models';
35 37
36 @Component({ 38 @Component({
@@ -52,22 +54,14 @@ import { @@ -52,22 +54,14 @@ import {
52 }) 54 })
53 export class FilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit { 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 @Input() disabled: boolean; 59 @Input() disabled: boolean;
60 60
61 @Input() 61 @Input()
62 set allowUserDynamicSource(allow: boolean) { 62 set allowUserDynamicSource(allow: boolean) {
63 - this.dynamicValueSourceTypes = [DynamicValueSourceType.CURRENT_TENANT,  
64 - DynamicValueSourceType.CURRENT_CUSTOMER]; 63 + this.dynamicValueSourceTypes = getDynamicSourcesForAllowUser(allow);
65 this.allow = allow; 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 private onlyUserDynamicSourceValue = false; 67 private onlyUserDynamicSourceValue = false;
@@ -92,8 +86,9 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, Vali @@ -92,8 +86,9 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, Vali
92 86
93 valueTypeEnum = EntityKeyValueType; 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 dynamicValueSourceTypeTranslations = dynamicValueSourceTypeTranslationMap; 93 dynamicValueSourceTypeTranslations = dynamicValueSourceTypeTranslationMap;
99 94
@@ -103,8 +98,6 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, Vali @@ -103,8 +98,6 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, Vali
103 98
104 inheritMode = false; 99 inheritMode = false;
105 100
106 - allow = true;  
107 -  
108 private propagateChange = null; 101 private propagateChange = null;
109 private propagateChangePending = false; 102 private propagateChangePending = false;
110 103
@@ -136,6 +136,7 @@ import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/emb @@ -136,6 +136,7 @@ import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/emb
136 import { EdgeDownlinkTableComponent } from '@home/components/edge/edge-downlink-table.component'; 136 import { EdgeDownlinkTableComponent } from '@home/components/edge/edge-downlink-table.component';
137 import { EdgeDownlinkTableHeaderComponent } from '@home/components/edge/edge-downlink-table-header.component'; 137 import { EdgeDownlinkTableHeaderComponent } from '@home/components/edge/edge-downlink-table-header.component';
138 import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component'; 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 import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component'; 140 import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component';
140 import { WidgetContainerComponent } from '@home/components/widget/widget-container.component'; 141 import { WidgetContainerComponent } from '@home/components/widget/widget-container.component';
141 import { SnmpDeviceProfileTransportModule } from '@home/components/profile/device/snpm/snmp-device-profile-transport.module'; 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,6 +240,7 @@ import { DeviceCredentialsModule } from '@home/components/device/device-credenti
239 AlarmScheduleInfoComponent, 240 AlarmScheduleInfoComponent,
240 DeviceProfileProvisionConfigurationComponent, 241 DeviceProfileProvisionConfigurationComponent,
241 AlarmScheduleComponent, 242 AlarmScheduleComponent,
  243 + AlarmDurationPredicateValueComponent,
242 DeviceWizardDialogComponent, 244 DeviceWizardDialogComponent,
243 AlarmScheduleDialogComponent, 245 AlarmScheduleDialogComponent,
244 EditAlarmDetailsDialogComponent, 246 EditAlarmDetailsDialogComponent,
@@ -348,6 +350,7 @@ import { DeviceCredentialsModule } from '@home/components/device/device-credenti @@ -348,6 +350,7 @@ import { DeviceCredentialsModule } from '@home/components/device/device-credenti
348 AlarmScheduleInfoComponent, 350 AlarmScheduleInfoComponent,
349 AlarmScheduleComponent, 351 AlarmScheduleComponent,
350 AlarmScheduleDialogComponent, 352 AlarmScheduleDialogComponent,
  353 + AlarmDurationPredicateValueComponent,
351 EditAlarmDetailsDialogComponent, 354 EditAlarmDetailsDialogComponent,
352 DeviceProfileProvisionConfigurationComponent, 355 DeviceProfileProvisionConfigurationComponent,
353 AlarmScheduleComponent, 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,7 +37,7 @@
37 [entityId]="entityId" 37 [entityId]="entityId"
38 formControlName="keyFilters"> 38 formControlName="keyFilters">
39 </tb-key-filter-list> 39 </tb-key-filter-list>
40 - <section formGroupName="spec" class="row"> 40 + <section formGroupName="spec" style="margin-top: 1em">
41 <mat-form-field class="mat-block" hideRequiredMarker> 41 <mat-form-field class="mat-block" hideRequiredMarker>
42 <mat-label translate>device-profile.condition-type</mat-label> 42 <mat-label translate>device-profile.condition-type</mat-label>
43 <mat-select formControlName="type" required> 43 <mat-select formControlName="type" required>
@@ -49,60 +49,26 @@ @@ -49,60 +49,26 @@
49 {{ 'device-profile.condition-type-required' | translate }} 49 {{ 'device-profile.condition-type-required' | translate }}
50 </mat-error> 50 </mat-error>
51 </mat-form-field> 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 </div> 72 </div>
107 </section> 73 </section>
108 </div> 74 </div>
@@ -43,12 +43,11 @@ export interface AlarmRuleConditionDialogData { @@ -43,12 +43,11 @@ export interface AlarmRuleConditionDialogData {
43 export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRuleConditionDialogComponent, AlarmCondition> 43 export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRuleConditionDialogComponent, AlarmCondition>
44 implements OnInit, ErrorStateMatcher { 44 implements OnInit, ErrorStateMatcher {
45 45
46 - timeUnits = Object.keys(TimeUnit); 46 + timeUnits = Object.values(TimeUnit);
47 timeUnitTranslations = timeUnitTranslationMap; 47 timeUnitTranslations = timeUnitTranslationMap;
48 - alarmConditionTypes = Object.keys(AlarmConditionType); 48 + alarmConditionTypes = Object.values(AlarmConditionType);
49 AlarmConditionType = AlarmConditionType; 49 AlarmConditionType = AlarmConditionType;
50 alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap; 50 alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap;
51 -  
52 readonly = this.data.readonly; 51 readonly = this.data.readonly;
53 condition = this.data.condition; 52 condition = this.data.condition;
54 entityId = this.data.entityId; 53 entityId = this.data.entityId;
@@ -70,9 +69,8 @@ export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRule @@ -70,9 +69,8 @@ export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRule
70 keyFilters: [keyFiltersToKeyFilterInfos(this.condition?.condition), Validators.required], 69 keyFilters: [keyFiltersToKeyFilterInfos(this.condition?.condition), Validators.required],
71 spec: this.fb.group({ 70 spec: this.fb.group({
72 type: [AlarmConditionType.SIMPLE, Validators.required], 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 this.conditionFormGroup.patchValue({spec: this.condition?.spec}); 76 this.conditionFormGroup.patchValue({spec: this.condition?.spec});
@@ -98,42 +96,37 @@ export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRule @@ -98,42 +96,37 @@ export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRule
98 private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) { 96 private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) {
99 switch (type) { 97 switch (type) {
100 case AlarmConditionType.DURATION: 98 case AlarmConditionType.DURATION:
101 - this.conditionFormGroup.get('spec.value').enable();  
102 this.conditionFormGroup.get('spec.unit').enable(); 99 this.conditionFormGroup.get('spec.unit').enable();
103 - this.conditionFormGroup.get('spec.count').disable(); 100 + this.conditionFormGroup.get('spec.predicate').enable();
104 if (resetDuration) { 101 if (resetDuration) {
105 this.conditionFormGroup.get('spec').patchValue({ 102 this.conditionFormGroup.get('spec').patchValue({
106 - count: null 103 + predicate: null
107 }); 104 });
108 } 105 }
109 break; 106 break;
110 case AlarmConditionType.REPEATING: 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 this.conditionFormGroup.get('spec.unit').disable(); 109 this.conditionFormGroup.get('spec.unit').disable();
114 if (resetDuration) { 110 if (resetDuration) {
115 this.conditionFormGroup.get('spec').patchValue({ 111 this.conditionFormGroup.get('spec').patchValue({
116 - value: null,  
117 - unit: null 112 + unit: null,
  113 + predicate: null
118 }); 114 });
119 } 115 }
120 break; 116 break;
121 case AlarmConditionType.SIMPLE: 117 case AlarmConditionType.SIMPLE:
122 - this.conditionFormGroup.get('spec.value').disable();  
123 this.conditionFormGroup.get('spec.unit').disable(); 118 this.conditionFormGroup.get('spec.unit').disable();
124 - this.conditionFormGroup.get('spec.count').disable(); 119 + this.conditionFormGroup.get('spec.predicate').disable();
125 if (resetDuration) { 120 if (resetDuration) {
126 this.conditionFormGroup.get('spec').patchValue({ 121 this.conditionFormGroup.get('spec').patchValue({
127 - value: null,  
128 unit: null, 122 unit: null,
129 - count: null 123 + predicate: null
130 }); 124 });
131 } 125 }
132 break; 126 break;
133 } 127 }
134 - this.conditionFormGroup.get('spec.value').updateValueAndValidity({emitEvent}); 128 + this.conditionFormGroup.get('spec.predicate').updateValueAndValidity({emitEvent});
135 this.conditionFormGroup.get('spec.unit').updateValueAndValidity({emitEvent}); 129 this.conditionFormGroup.get('spec.unit').updateValueAndValidity({emitEvent});
136 - this.conditionFormGroup.get('spec.count').updateValueAndValidity({emitEvent});  
137 } 130 }
138 131
139 cancel(): void { 132 cancel(): void {
@@ -18,22 +18,25 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core'; @@ -18,22 +18,25 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core';
18 import { 18 import {
19 ControlValueAccessor, 19 ControlValueAccessor,
20 FormBuilder, 20 FormBuilder,
21 - FormControl, FormGroup, 21 + FormControl,
  22 + FormGroup,
22 NG_VALIDATORS, 23 NG_VALIDATORS,
23 NG_VALUE_ACCESSOR, 24 NG_VALUE_ACCESSOR,
24 - Validator, Validators 25 + Validator,
  26 + Validators
25 } from '@angular/forms'; 27 } from '@angular/forms';
26 import { MatDialog } from '@angular/material/dialog'; 28 import { MatDialog } from '@angular/material/dialog';
27 import { deepClone, isUndefined } from '@core/utils'; 29 import { deepClone, isUndefined } from '@core/utils';
28 import { TranslateService } from '@ngx-translate/core'; 30 import { TranslateService } from '@ngx-translate/core';
29 import { DatePipe } from '@angular/common'; 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 import { 33 import {
32 AlarmRuleConditionDialogComponent, 34 AlarmRuleConditionDialogComponent,
33 AlarmRuleConditionDialogData 35 AlarmRuleConditionDialogData
34 } from '@home/components/profile/alarm/alarm-rule-condition-dialog.component'; 36 } from '@home/components/profile/alarm/alarm-rule-condition-dialog.component';
35 import { TimeUnit } from '@shared/models/time/time.models'; 37 import { TimeUnit } from '@shared/models/time/time.models';
36 import { EntityId } from '@shared/models/id/entity-id'; 38 import { EntityId } from '@shared/models/id/entity-id';
  39 +import { dynamicValueSourceTypeTranslationMap } from '@shared/models/query/query.models';
37 40
38 @Component({ 41 @Component({
39 selector: 'tb-alarm-rule-condition', 42 selector: 'tb-alarm-rule-condition',
@@ -159,22 +162,43 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit @@ -159,22 +162,43 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
159 let duringText = ''; 162 let duringText = '';
160 switch (spec.unit) { 163 switch (spec.unit) {
161 case TimeUnit.SECONDS: 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 break; 166 break;
164 case TimeUnit.MINUTES: 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 break; 169 break;
167 case TimeUnit.HOURS: 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 break; 172 break;
170 case TimeUnit.DAYS: 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 break; 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 break; 189 break;
176 case AlarmConditionType.REPEATING: 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 break; 202 break;
179 } 203 }
180 } 204 }
@@ -15,7 +15,8 @@ @@ -15,7 +15,8 @@
15 limitations under the License. 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 <mat-card fxFlex="initial" class="tb-request-password-reset-card"> 20 <mat-card fxFlex="initial" class="tb-request-password-reset-card">
20 <mat-card-title class="layout-padding"> 21 <mat-card-title class="layout-padding">
21 <span translate class="mat-headline">login.request-password-reset</span> 22 <span translate class="mat-headline">login.request-password-reset</span>
@@ -38,7 +39,7 @@ @@ -38,7 +39,7 @@
38 </mat-form-field> 39 </mat-form-field>
39 <div fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="16px" fxLayoutAlign="start center" 40 <div fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="16px" fxLayoutAlign="start center"
40 fxLayoutAlign.gt-xs="center start"> 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 {{ 'login.request-password-reset' | translate }} 43 {{ 'login.request-password-reset' | translate }}
43 </button> 44 </button>
44 <button mat-raised-button color="primary" type="button" [disabled]="(isLoading$ | async)" 45 <button mat-raised-button color="primary" type="button" [disabled]="(isLoading$ | async)"
@@ -30,6 +30,8 @@ import { TranslateService } from '@ngx-translate/core'; @@ -30,6 +30,8 @@ import { TranslateService } from '@ngx-translate/core';
30 }) 30 })
31 export class ResetPasswordRequestComponent extends PageComponent implements OnInit { 31 export class ResetPasswordRequestComponent extends PageComponent implements OnInit {
32 32
  33 + clicked: boolean = false;
  34 +
33 requestPasswordRequest = this.fb.group({ 35 requestPasswordRequest = this.fb.group({
34 email: ['', [Validators.email, Validators.required]] 36 email: ['', [Validators.email, Validators.required]]
35 }, {updateOn: 'submit'}); 37 }, {updateOn: 'submit'});
@@ -44,8 +46,14 @@ export class ResetPasswordRequestComponent extends PageComponent implements OnIn @@ -44,8 +46,14 @@ export class ResetPasswordRequestComponent extends PageComponent implements OnIn
44 ngOnInit() { 46 ngOnInit() {
45 } 47 }
46 48
  49 + disableInputs() {
  50 + this.requestPasswordRequest.disable();
  51 + this.clicked = true;
  52 + }
  53 +
47 sendResetPasswordLink() { 54 sendResetPasswordLink() {
48 if (this.requestPasswordRequest.valid) { 55 if (this.requestPasswordRequest.valid) {
  56 + this.disableInputs();
49 this.authService.sendResetPasswordLink(this.requestPasswordRequest.get('email').value).subscribe( 57 this.authService.sendResetPasswordLink(this.requestPasswordRequest.get('email').value).subscribe(
50 () => { 58 () => {
51 this.store.dispatch(new ActionNotificationShow({ 59 this.store.dispatch(new ActionNotificationShow({
@@ -23,7 +23,7 @@ import { EntitySearchQuery } from '@shared/models/relation.models'; @@ -23,7 +23,7 @@ import { EntitySearchQuery } from '@shared/models/relation.models';
23 import { DeviceProfileId } from '@shared/models/id/device-profile-id'; 23 import { DeviceProfileId } from '@shared/models/id/device-profile-id';
24 import { RuleChainId } from '@shared/models/id/rule-chain-id'; 24 import { RuleChainId } from '@shared/models/id/rule-chain-id';
25 import { EntityInfoData } from '@shared/models/entity.models'; 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 import { TimeUnit } from '@shared/models/time/time.models'; 27 import { TimeUnit } from '@shared/models/time/time.models';
28 import * as _moment from 'moment'; 28 import * as _moment from 'moment';
29 import { AbstractControl, ValidationErrors } from '@angular/forms'; 29 import { AbstractControl, ValidationErrors } from '@angular/forms';
@@ -424,8 +424,7 @@ export const AlarmConditionTypeTranslationMap = new Map<AlarmConditionType, stri @@ -424,8 +424,7 @@ export const AlarmConditionTypeTranslationMap = new Map<AlarmConditionType, stri
424 export interface AlarmConditionSpec{ 424 export interface AlarmConditionSpec{
425 type?: AlarmConditionType; 425 type?: AlarmConditionType;
426 unit?: TimeUnit; 426 unit?: TimeUnit;
427 - value?: number;  
428 - count?: number; 427 + predicate: FilterPredicateValue<number>;
429 } 428 }
430 429
431 export interface AlarmCondition { 430 export interface AlarmCondition {
@@ -202,6 +202,17 @@ export function createDefaultFilterPredicate(valueType: EntityKeyValueType, comp @@ -202,6 +202,17 @@ export function createDefaultFilterPredicate(valueType: EntityKeyValueType, comp
202 return predicate; 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 export enum FilterPredicateType { 216 export enum FilterPredicateType {
206 STRING = 'STRING', 217 STRING = 'STRING',
207 NUMERIC = 'NUMERIC', 218 NUMERIC = 'NUMERIC',
@@ -289,6 +300,10 @@ export const dynamicValueSourceTypeTranslationMap = new Map<DynamicValueSourceTy @@ -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 export interface DynamicValue<T> { 307 export interface DynamicValue<T> {
293 sourceType: DynamicValueSourceType; 308 sourceType: DynamicValueSourceType;
294 sourceAttribute: string; 309 sourceAttribute: string;
@@ -1178,6 +1178,7 @@ @@ -1178,6 +1178,7 @@
1178 "condition-type-simple": "Simple", 1178 "condition-type-simple": "Simple",
1179 "condition-type-duration": "Duration", 1179 "condition-type-duration": "Duration",
1180 "condition-during": "During {{during}}", 1180 "condition-during": "During {{during}}",
  1181 + "condition-during-dynamic": "During \"{{ attribute }}\" ({{during}})",
1181 "condition-type-repeating": "Repeating", 1182 "condition-type-repeating": "Repeating",
1182 "condition-type-required": "Condition type is required.", 1183 "condition-type-required": "Condition type is required.",
1183 "condition-repeating-value": "Count of events", 1184 "condition-repeating-value": "Count of events",
@@ -1185,6 +1186,7 @@ @@ -1185,6 +1186,7 @@
1185 "condition-repeating-value-pattern": "Count of events should be integers.", 1186 "condition-repeating-value-pattern": "Count of events should be integers.",
1186 "condition-repeating-value-required": "Count of events is required.", 1187 "condition-repeating-value-required": "Count of events is required.",
1187 "condition-repeat-times": "Repeats { count, plural, 1 {1 time} other {# times} }", 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 "schedule-type": "Scheduler type", 1190 "schedule-type": "Scheduler type",
1189 "schedule-type-required": "Scheduler type is required.", 1191 "schedule-type-required": "Scheduler type is required.",
1190 "schedule": "Schedule", 1192 "schedule": "Schedule",
@@ -2268,7 +2270,7 @@ @@ -2268,7 +2270,7 @@
2268 "expired-password-reset-message": "Your credentials has been expired! Please create new password.", 2270 "expired-password-reset-message": "Your credentials has been expired! Please create new password.",
2269 "new-password": "New password", 2271 "new-password": "New password",
2270 "new-password-again": "New password again", 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 "email": "Email", 2274 "email": "Email",
2273 "login-with": "Login with {{name}}", 2275 "login-with": "Login with {{name}}",
2274 "or": "or", 2276 "or": "or",