Commit bccbecadf4d8473ef2fc5b33c706877a73824f08
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; |
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java
@@ -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, |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.html
0 → 100644
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> |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.scss
0 → 100644
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 | +} |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.ts
0 → 100644
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", |