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