Commit 1c2ec66f16cc50e1d352612f7e94f09adca85709
Merge remote-tracking branch 'upstream/master' into improvement/oauth2
Showing
53 changed files
with
1503 additions
and
649 deletions
@@ -469,6 +469,14 @@ class DefaultTbContext implements TbContext { | @@ -469,6 +469,14 @@ class DefaultTbContext implements TbContext { | ||
469 | return mainCtx.getRuleNodeStateService().save(getTenantId(), state); | 469 | return mainCtx.getRuleNodeStateService().save(getTenantId(), state); |
470 | } | 470 | } |
471 | 471 | ||
472 | + @Override | ||
473 | + public void clearRuleNodeStates() { | ||
474 | + if (log.isDebugEnabled()) { | ||
475 | + log.debug("[{}][{}] Going to clear rule node states", getTenantId(), getSelfId()); | ||
476 | + } | ||
477 | + mainCtx.getRuleNodeStateService().removeByRuleNodeId(getTenantId(), getSelfId()); | ||
478 | + } | ||
479 | + | ||
472 | private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) { | 480 | private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) { |
473 | TbMsgMetaData metaData = new TbMsgMetaData(); | 481 | TbMsgMetaData metaData = new TbMsgMetaData(); |
474 | metaData.putValue("ruleNodeId", ruleNodeId.toString()); | 482 | metaData.putValue("ruleNodeId", ruleNodeId.toString()); |
@@ -164,6 +164,8 @@ public class RuleChainController extends BaseController { | @@ -164,6 +164,8 @@ public class RuleChainController extends BaseController { | ||
164 | 164 | ||
165 | RuleChain savedRuleChain = installScripts.createDefaultRuleChain(getCurrentUser().getTenantId(), request.getName()); | 165 | RuleChain savedRuleChain = installScripts.createDefaultRuleChain(getCurrentUser().getTenantId(), request.getName()); |
166 | 166 | ||
167 | + tbClusterService.onEntityStateChange(savedRuleChain.getTenantId(), savedRuleChain.getId(), ComponentLifecycleEvent.CREATED); | ||
168 | + | ||
167 | logEntityAction(savedRuleChain.getId(), savedRuleChain, null, ActionType.ADDED, null); | 169 | logEntityAction(savedRuleChain.getId(), savedRuleChain, null, ActionType.ADDED, null); |
168 | 170 | ||
169 | return savedRuleChain; | 171 | return savedRuleChain; |
@@ -42,6 +42,7 @@ import java.nio.file.DirectoryStream; | @@ -42,6 +42,7 @@ import java.nio.file.DirectoryStream; | ||
42 | import java.nio.file.Files; | 42 | import java.nio.file.Files; |
43 | import java.nio.file.Path; | 43 | import java.nio.file.Path; |
44 | import java.nio.file.Paths; | 44 | import java.nio.file.Paths; |
45 | +import java.util.Optional; | ||
45 | 46 | ||
46 | import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper; | 47 | import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper; |
47 | 48 | ||
@@ -243,6 +244,11 @@ public class InstallScripts { | @@ -243,6 +244,11 @@ public class InstallScripts { | ||
243 | try { | 244 | try { |
244 | JsonNode oauth2ConfigTemplateJson = objectMapper.readTree(path.toFile()); | 245 | JsonNode oauth2ConfigTemplateJson = objectMapper.readTree(path.toFile()); |
245 | OAuth2ClientRegistrationTemplate clientRegistrationTemplate = objectMapper.treeToValue(oauth2ConfigTemplateJson, OAuth2ClientRegistrationTemplate.class); | 246 | OAuth2ClientRegistrationTemplate clientRegistrationTemplate = objectMapper.treeToValue(oauth2ConfigTemplateJson, OAuth2ClientRegistrationTemplate.class); |
247 | + Optional<OAuth2ClientRegistrationTemplate> existingClientRegistrationTemplate = | ||
248 | + oAuth2TemplateService.findClientRegistrationTemplateByProviderId(clientRegistrationTemplate.getProviderId()); | ||
249 | + if (existingClientRegistrationTemplate.isPresent()) { | ||
250 | + clientRegistrationTemplate.setId(existingClientRegistrationTemplate.get().getId()); | ||
251 | + } | ||
246 | oAuth2TemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate); | 252 | oAuth2TemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate); |
247 | } catch (Exception e) { | 253 | } catch (Exception e) { |
248 | log.error("Unable to load oauth2 config templates from json: [{}]", path.toString()); | 254 | log.error("Unable to load oauth2 config templates from json: [{}]", path.toString()); |
@@ -19,10 +19,13 @@ import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; | @@ -19,10 +19,13 @@ import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; | ||
19 | import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; | 19 | import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; |
20 | 20 | ||
21 | import java.util.List; | 21 | import java.util.List; |
22 | +import java.util.Optional; | ||
22 | 23 | ||
23 | public interface OAuth2ConfigTemplateService { | 24 | public interface OAuth2ConfigTemplateService { |
24 | OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate); | 25 | OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate); |
25 | 26 | ||
27 | + Optional<OAuth2ClientRegistrationTemplate> findClientRegistrationTemplateByProviderId(String providerId); | ||
28 | + | ||
26 | OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId); | 29 | OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId); |
27 | 30 | ||
28 | List<OAuth2ClientRegistrationTemplate> findAllClientRegistrationTemplates(); | 31 | List<OAuth2ClientRegistrationTemplate> findAllClientRegistrationTemplates(); |
@@ -30,4 +30,5 @@ public interface RuleNodeStateService { | @@ -30,4 +30,5 @@ public interface RuleNodeStateService { | ||
30 | 30 | ||
31 | RuleNodeState save(TenantId tenantId, RuleNodeState ruleNodeState); | 31 | RuleNodeState save(TenantId tenantId, RuleNodeState ruleNodeState); |
32 | 32 | ||
33 | + void removeByRuleNodeId(TenantId tenantId, RuleNodeId selfId); | ||
33 | } | 34 | } |
@@ -18,5 +18,6 @@ package org.thingsboard.server.common.data.query; | @@ -18,5 +18,6 @@ package org.thingsboard.server.common.data.query; | ||
18 | public enum DynamicValueSourceType { | 18 | public enum DynamicValueSourceType { |
19 | CURRENT_TENANT, | 19 | CURRENT_TENANT, |
20 | CURRENT_CUSTOMER, | 20 | CURRENT_CUSTOMER, |
21 | - CURRENT_USER | 21 | + CURRENT_USER, |
22 | + CURRENT_DEVICE | ||
22 | } | 23 | } |
@@ -19,7 +19,11 @@ import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplat | @@ -19,7 +19,11 @@ import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplat | ||
19 | import org.thingsboard.server.dao.Dao; | 19 | import org.thingsboard.server.dao.Dao; |
20 | 20 | ||
21 | import java.util.List; | 21 | import java.util.List; |
22 | +import java.util.Optional; | ||
22 | 23 | ||
23 | public interface OAuth2ClientRegistrationTemplateDao extends Dao<OAuth2ClientRegistrationTemplate> { | 24 | public interface OAuth2ClientRegistrationTemplateDao extends Dao<OAuth2ClientRegistrationTemplate> { |
25 | + | ||
26 | + Optional<OAuth2ClientRegistrationTemplate> findByProviderId(String providerId); | ||
27 | + | ||
24 | List<OAuth2ClientRegistrationTemplate> findAll(); | 28 | List<OAuth2ClientRegistrationTemplate> findAll(); |
25 | } | 29 | } |
@@ -29,6 +29,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; | @@ -29,6 +29,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; | ||
29 | import org.thingsboard.server.dao.service.DataValidator; | 29 | import org.thingsboard.server.dao.service.DataValidator; |
30 | 30 | ||
31 | import java.util.List; | 31 | import java.util.List; |
32 | +import java.util.Optional; | ||
32 | 33 | ||
33 | import static org.thingsboard.server.dao.service.Validator.validateId; | 34 | import static org.thingsboard.server.dao.service.Validator.validateId; |
34 | import static org.thingsboard.server.dao.service.Validator.validateString; | 35 | import static org.thingsboard.server.dao.service.Validator.validateString; |
@@ -37,6 +38,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString; | @@ -37,6 +38,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString; | ||
37 | @Service | 38 | @Service |
38 | public class OAuth2ConfigTemplateServiceImpl extends AbstractEntityService implements OAuth2ConfigTemplateService { | 39 | public class OAuth2ConfigTemplateServiceImpl extends AbstractEntityService implements OAuth2ConfigTemplateService { |
39 | public static final String INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID = "Incorrect clientRegistrationTemplateId "; | 40 | public static final String INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID = "Incorrect clientRegistrationTemplateId "; |
41 | + public static final String INCORRECT_CLIENT_REGISTRATION_PROVIDER_ID = "Incorrect clientRegistrationProviderId "; | ||
40 | 42 | ||
41 | @Autowired | 43 | @Autowired |
42 | private OAuth2ClientRegistrationTemplateDao clientRegistrationTemplateDao; | 44 | private OAuth2ClientRegistrationTemplateDao clientRegistrationTemplateDao; |
@@ -60,6 +62,13 @@ public class OAuth2ConfigTemplateServiceImpl extends AbstractEntityService imple | @@ -60,6 +62,13 @@ public class OAuth2ConfigTemplateServiceImpl extends AbstractEntityService imple | ||
60 | } | 62 | } |
61 | 63 | ||
62 | @Override | 64 | @Override |
65 | + public Optional<OAuth2ClientRegistrationTemplate> findClientRegistrationTemplateByProviderId(String providerId) { | ||
66 | + log.trace("Executing findClientRegistrationTemplateByProviderId [{}]", providerId); | ||
67 | + validateString(providerId, INCORRECT_CLIENT_REGISTRATION_PROVIDER_ID + providerId); | ||
68 | + return clientRegistrationTemplateDao.findByProviderId(providerId); | ||
69 | + } | ||
70 | + | ||
71 | + @Override | ||
63 | public OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId) { | 72 | public OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId) { |
64 | log.trace("Executing findClientRegistrationTemplateById [{}]", templateId); | 73 | log.trace("Executing findClientRegistrationTemplateById [{}]", templateId); |
65 | validateId(templateId, INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID + templateId); | 74 | validateId(templateId, INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID + templateId); |
@@ -68,6 +68,17 @@ public class BaseRuleNodeStateService extends AbstractEntityService implements R | @@ -68,6 +68,17 @@ public class BaseRuleNodeStateService extends AbstractEntityService implements R | ||
68 | return saveOrUpdate(tenantId, ruleNodeState, false); | 68 | return saveOrUpdate(tenantId, ruleNodeState, false); |
69 | } | 69 | } |
70 | 70 | ||
71 | + @Override | ||
72 | + public void removeByRuleNodeId(TenantId tenantId, RuleNodeId ruleNodeId) { | ||
73 | + if (tenantId == null) { | ||
74 | + throw new DataValidationException("Tenant id should be specified!."); | ||
75 | + } | ||
76 | + if (ruleNodeId == null) { | ||
77 | + throw new DataValidationException("Rule node id should be specified!."); | ||
78 | + } | ||
79 | + ruleNodeStateDao.removeByRuleNodeId(ruleNodeId.getId()); | ||
80 | + } | ||
81 | + | ||
71 | public RuleNodeState saveOrUpdate(TenantId tenantId, RuleNodeState ruleNodeState, boolean update) { | 82 | public RuleNodeState saveOrUpdate(TenantId tenantId, RuleNodeState ruleNodeState, boolean update) { |
72 | try { | 83 | try { |
73 | if (update) { | 84 | if (update) { |
@@ -16,6 +16,8 @@ | @@ -16,6 +16,8 @@ | ||
16 | package org.thingsboard.server.dao.rule; | 16 | package org.thingsboard.server.dao.rule; |
17 | 17 | ||
18 | import org.thingsboard.server.common.data.id.EntityId; | 18 | import org.thingsboard.server.common.data.id.EntityId; |
19 | +import org.thingsboard.server.common.data.id.RuleNodeId; | ||
20 | +import org.thingsboard.server.common.data.id.TenantId; | ||
19 | import org.thingsboard.server.common.data.page.PageData; | 21 | import org.thingsboard.server.common.data.page.PageData; |
20 | import org.thingsboard.server.common.data.page.PageLink; | 22 | import org.thingsboard.server.common.data.page.PageLink; |
21 | import org.thingsboard.server.common.data.rule.RuleNodeState; | 23 | import org.thingsboard.server.common.data.rule.RuleNodeState; |
@@ -31,4 +33,6 @@ public interface RuleNodeStateDao extends Dao<RuleNodeState> { | @@ -31,4 +33,6 @@ public interface RuleNodeStateDao extends Dao<RuleNodeState> { | ||
31 | PageData<RuleNodeState> findByRuleNodeId(UUID ruleNodeId, PageLink pageLink); | 33 | PageData<RuleNodeState> findByRuleNodeId(UUID ruleNodeId, PageLink pageLink); |
32 | 34 | ||
33 | RuleNodeState findByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId); | 35 | RuleNodeState findByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId); |
36 | + | ||
37 | + void removeByRuleNodeId(UUID ruleNodeId); | ||
34 | } | 38 | } |
@@ -26,6 +26,7 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao; | @@ -26,6 +26,7 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao; | ||
26 | 26 | ||
27 | import java.util.ArrayList; | 27 | import java.util.ArrayList; |
28 | import java.util.List; | 28 | import java.util.List; |
29 | +import java.util.Optional; | ||
29 | import java.util.UUID; | 30 | import java.util.UUID; |
30 | 31 | ||
31 | @Component | 32 | @Component |
@@ -44,6 +45,12 @@ public class JpaOAuth2ClientRegistrationTemplateDao extends JpaAbstractDao<OAuth | @@ -44,6 +45,12 @@ public class JpaOAuth2ClientRegistrationTemplateDao extends JpaAbstractDao<OAuth | ||
44 | } | 45 | } |
45 | 46 | ||
46 | @Override | 47 | @Override |
48 | + public Optional<OAuth2ClientRegistrationTemplate> findByProviderId(String providerId) { | ||
49 | + OAuth2ClientRegistrationTemplate oAuth2ClientRegistrationTemplate = DaoUtil.getData(repository.findByProviderId(providerId)); | ||
50 | + return Optional.ofNullable(oAuth2ClientRegistrationTemplate); | ||
51 | + } | ||
52 | + | ||
53 | + @Override | ||
47 | public List<OAuth2ClientRegistrationTemplate> findAll() { | 54 | public List<OAuth2ClientRegistrationTemplate> findAll() { |
48 | Iterable<OAuth2ClientRegistrationTemplateEntity> entities = repository.findAll(); | 55 | Iterable<OAuth2ClientRegistrationTemplateEntity> entities = repository.findAll(); |
49 | List<OAuth2ClientRegistrationTemplate> result = new ArrayList<>(); | 56 | List<OAuth2ClientRegistrationTemplate> result = new ArrayList<>(); |
@@ -21,4 +21,7 @@ import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationTemplateEnti | @@ -21,4 +21,7 @@ import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationTemplateEnti | ||
21 | import java.util.UUID; | 21 | import java.util.UUID; |
22 | 22 | ||
23 | public interface OAuth2ClientRegistrationTemplateRepository extends CrudRepository<OAuth2ClientRegistrationTemplateEntity, UUID> { | 23 | public interface OAuth2ClientRegistrationTemplateRepository extends CrudRepository<OAuth2ClientRegistrationTemplateEntity, UUID> { |
24 | + | ||
25 | + OAuth2ClientRegistrationTemplateEntity findByProviderId(String providerId); | ||
26 | + | ||
24 | } | 27 | } |
@@ -472,13 +472,13 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { | @@ -472,13 +472,13 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { | ||
472 | if (entityFilter.isFetchLastLevelOnly()) { | 472 | if (entityFilter.isFetchLastLevelOnly()) { |
473 | String fromOrTo = (entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "from" : "to"); | 473 | String fromOrTo = (entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "from" : "to"); |
474 | StringBuilder notExistsPart = new StringBuilder(); | 474 | StringBuilder notExistsPart = new StringBuilder(); |
475 | - notExistsPart.append(" NOT EXISTS (SELECT 1 from relation nr where ") | 475 | + notExistsPart.append(" NOT EXISTS (SELECT 1 from relation nr ") |
476 | + .append(whereFilter.replaceAll("re\\.", "nr\\.")) | ||
477 | + .append(" and ") | ||
476 | .append("nr.").append(fromOrTo).append("_id").append(" = re.").append(toOrFrom).append("_id") | 478 | .append("nr.").append(fromOrTo).append("_id").append(" = re.").append(toOrFrom).append("_id") |
477 | .append(" and ") | 479 | .append(" and ") |
478 | .append("nr.").append(fromOrTo).append("_type").append(" = re.").append(toOrFrom).append("_type"); | 480 | .append("nr.").append(fromOrTo).append("_type").append(" = re.").append(toOrFrom).append("_type"); |
479 | - if (!StringUtils.isEmpty(entityFilter.getRelationType())) { | ||
480 | - notExistsPart.append(" and nr.relation_type = :where_relation_type"); | ||
481 | - } | 481 | + |
482 | notExistsPart.append(")"); | 482 | notExistsPart.append(")"); |
483 | whereFilter += " and ( re.lvl = " + entityFilter.getMaxLevel() + " OR " + notExistsPart.toString() + ")"; | 483 | whereFilter += " and ( re.lvl = " + entityFilter.getMaxLevel() + " OR " + notExistsPart.toString() + ")"; |
484 | } | 484 | } |
@@ -551,12 +551,12 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { | @@ -551,12 +551,12 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { | ||
551 | 551 | ||
552 | StringBuilder notExistsPart = new StringBuilder(); | 552 | StringBuilder notExistsPart = new StringBuilder(); |
553 | notExistsPart.append(" NOT EXISTS (SELECT 1 from relation nr WHERE "); | 553 | notExistsPart.append(" NOT EXISTS (SELECT 1 from relation nr WHERE "); |
554 | - notExistsPart.append(whereFilter.toString()); | ||
555 | notExistsPart | 554 | notExistsPart |
556 | - .append(" and ") | ||
557 | .append("nr.").append(fromOrTo).append("_id").append(" = re.").append(toOrFrom).append("_id") | 555 | .append("nr.").append(fromOrTo).append("_id").append(" = re.").append(toOrFrom).append("_id") |
558 | .append(" and ") | 556 | .append(" and ") |
559 | - .append("nr.").append(fromOrTo).append("_type").append(" = re.").append(toOrFrom).append("_type"); | 557 | + .append("nr.").append(fromOrTo).append("_type").append(" = re.").append(toOrFrom).append("_type") |
558 | + .append(" and ") | ||
559 | + .append(whereFilter.toString().replaceAll("re\\.", "nr\\.")); | ||
560 | 560 | ||
561 | notExistsPart.append(")"); | 561 | notExistsPart.append(")"); |
562 | whereFilter.append(" and ( re.lvl = ").append(entityFilter.getMaxLevel()).append(" OR ").append(notExistsPart.toString()).append(")"); | 562 | whereFilter.append(" and ( re.lvl = ").append(entityFilter.getMaxLevel()).append(" OR ").append(notExistsPart.toString()).append(")"); |
@@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; | @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; | ||
19 | import org.springframework.beans.factory.annotation.Autowired; | 19 | import org.springframework.beans.factory.annotation.Autowired; |
20 | import org.springframework.data.repository.CrudRepository; | 20 | import org.springframework.data.repository.CrudRepository; |
21 | import org.springframework.stereotype.Component; | 21 | import org.springframework.stereotype.Component; |
22 | +import org.springframework.transaction.annotation.Transactional; | ||
22 | import org.thingsboard.server.common.data.id.EntityId; | 23 | import org.thingsboard.server.common.data.id.EntityId; |
23 | import org.thingsboard.server.common.data.page.PageData; | 24 | import org.thingsboard.server.common.data.page.PageData; |
24 | import org.thingsboard.server.common.data.page.PageLink; | 25 | import org.thingsboard.server.common.data.page.PageLink; |
@@ -56,4 +57,10 @@ public class JpaRuleNodeStateDao extends JpaAbstractDao<RuleNodeStateEntity, Rul | @@ -56,4 +57,10 @@ public class JpaRuleNodeStateDao extends JpaAbstractDao<RuleNodeStateEntity, Rul | ||
56 | public RuleNodeState findByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId) { | 57 | public RuleNodeState findByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId) { |
57 | return DaoUtil.getData(ruleNodeStateRepository.findByRuleNodeIdAndEntityId(ruleNodeId, entityId)); | 58 | return DaoUtil.getData(ruleNodeStateRepository.findByRuleNodeIdAndEntityId(ruleNodeId, entityId)); |
58 | } | 59 | } |
60 | + | ||
61 | + @Transactional | ||
62 | + @Override | ||
63 | + public void removeByRuleNodeId(UUID ruleNodeId) { | ||
64 | + ruleNodeStateRepository.removeByRuleNodeId(ruleNodeId); | ||
65 | + } | ||
59 | } | 66 | } |
@@ -33,4 +33,7 @@ public interface RuleNodeStateRepository extends PagingAndSortingRepository<Rule | @@ -33,4 +33,7 @@ public interface RuleNodeStateRepository extends PagingAndSortingRepository<Rule | ||
33 | 33 | ||
34 | @Query("SELECT e FROM RuleNodeStateEntity e WHERE e.ruleNodeId = :ruleNodeId and e.entityId = :entityId") | 34 | @Query("SELECT e FROM RuleNodeStateEntity e WHERE e.ruleNodeId = :ruleNodeId and e.entityId = :entityId") |
35 | RuleNodeStateEntity findByRuleNodeIdAndEntityId(@Param("ruleNodeId") UUID ruleNodeId, @Param("entityId") UUID entityId); | 35 | RuleNodeStateEntity findByRuleNodeIdAndEntityId(@Param("ruleNodeId") UUID ruleNodeId, @Param("entityId") UUID entityId); |
36 | + | ||
37 | + void removeByRuleNodeId(@Param("ruleNodeId") UUID ruleNodeId); | ||
38 | + | ||
36 | } | 39 | } |
@@ -221,4 +221,6 @@ public interface TbContext { | @@ -221,4 +221,6 @@ public interface TbContext { | ||
221 | RuleNodeState findRuleNodeStateForEntity(EntityId entityId); | 221 | RuleNodeState findRuleNodeStateForEntity(EntityId entityId); |
222 | 222 | ||
223 | RuleNodeState saveRuleNodeState(RuleNodeState state); | 223 | RuleNodeState saveRuleNodeState(RuleNodeState state); |
224 | + | ||
225 | + void clearRuleNodeStates(); | ||
224 | } | 226 | } |
@@ -29,6 +29,9 @@ import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpe | @@ -29,6 +29,9 @@ import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpe | ||
29 | import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule; | 29 | import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule; |
30 | import org.thingsboard.server.common.data.query.BooleanFilterPredicate; | 30 | import org.thingsboard.server.common.data.query.BooleanFilterPredicate; |
31 | import org.thingsboard.server.common.data.query.ComplexFilterPredicate; | 31 | import org.thingsboard.server.common.data.query.ComplexFilterPredicate; |
32 | +import org.thingsboard.server.common.data.query.EntityKey; | ||
33 | +import org.thingsboard.server.common.data.query.EntityKeyType; | ||
34 | +import org.thingsboard.server.common.data.query.FilterPredicateValue; | ||
32 | import org.thingsboard.server.common.data.query.KeyFilter; | 35 | import org.thingsboard.server.common.data.query.KeyFilter; |
33 | import org.thingsboard.server.common.data.query.KeyFilterPredicate; | 36 | import org.thingsboard.server.common.data.query.KeyFilterPredicate; |
34 | import org.thingsboard.server.common.data.query.NumericFilterPredicate; | 37 | import org.thingsboard.server.common.data.query.NumericFilterPredicate; |
@@ -38,22 +41,25 @@ import org.thingsboard.server.common.msg.tools.SchedulerUtils; | @@ -38,22 +41,25 @@ import org.thingsboard.server.common.msg.tools.SchedulerUtils; | ||
38 | import java.time.Instant; | 41 | import java.time.Instant; |
39 | import java.time.ZoneId; | 42 | import java.time.ZoneId; |
40 | import java.time.ZonedDateTime; | 43 | import java.time.ZonedDateTime; |
41 | -import java.util.Calendar; | 44 | +import java.util.Set; |
45 | +import java.util.function.Function; | ||
42 | 46 | ||
43 | @Data | 47 | @Data |
44 | -public class AlarmRuleState { | 48 | +class AlarmRuleState { |
45 | 49 | ||
46 | private final AlarmSeverity severity; | 50 | private final AlarmSeverity severity; |
47 | private final AlarmRule alarmRule; | 51 | private final AlarmRule alarmRule; |
48 | private final AlarmConditionSpec spec; | 52 | private final AlarmConditionSpec spec; |
49 | private final long requiredDurationInMs; | 53 | private final long requiredDurationInMs; |
50 | private final long requiredRepeats; | 54 | private final long requiredRepeats; |
55 | + private final Set<EntityKey> entityKeys; | ||
51 | private PersistedAlarmRuleState state; | 56 | private PersistedAlarmRuleState state; |
52 | private boolean updateFlag; | 57 | private boolean updateFlag; |
53 | 58 | ||
54 | - public AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, PersistedAlarmRuleState state) { | 59 | + AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, Set<EntityKey> entityKeys, PersistedAlarmRuleState state) { |
55 | this.severity = severity; | 60 | this.severity = severity; |
56 | this.alarmRule = alarmRule; | 61 | this.alarmRule = alarmRule; |
62 | + this.entityKeys = entityKeys; | ||
57 | if (state != null) { | 63 | if (state != null) { |
58 | this.state = state; | 64 | this.state = state; |
59 | } else { | 65 | } else { |
@@ -76,6 +82,30 @@ public class AlarmRuleState { | @@ -76,6 +82,30 @@ public class AlarmRuleState { | ||
76 | this.requiredRepeats = requiredRepeats; | 82 | this.requiredRepeats = requiredRepeats; |
77 | } | 83 | } |
78 | 84 | ||
85 | + public boolean validateTsUpdate(Set<EntityKey> changedKeys) { | ||
86 | + for (EntityKey key : changedKeys) { | ||
87 | + if (entityKeys.contains(key)) { | ||
88 | + return true; | ||
89 | + } | ||
90 | + } | ||
91 | + return false; | ||
92 | + } | ||
93 | + | ||
94 | + public boolean validateAttrUpdate(Set<EntityKey> changedKeys) { | ||
95 | + //If the attribute was updated, but no new telemetry arrived - we ignore this until new telemetry is there. | ||
96 | + for (EntityKey key : entityKeys) { | ||
97 | + if (key.getType().equals(EntityKeyType.TIME_SERIES)) { | ||
98 | + return false; | ||
99 | + } | ||
100 | + } | ||
101 | + for (EntityKey key : changedKeys) { | ||
102 | + if (entityKeys.contains(key)) { | ||
103 | + return true; | ||
104 | + } | ||
105 | + } | ||
106 | + return false; | ||
107 | + } | ||
108 | + | ||
79 | public AlarmConditionSpec getSpec(AlarmRule alarmRule) { | 109 | public AlarmConditionSpec getSpec(AlarmRule alarmRule) { |
80 | AlarmConditionSpec spec = alarmRule.getCondition().getSpec(); | 110 | AlarmConditionSpec spec = alarmRule.getCondition().getSpec(); |
81 | if (spec == null) { | 111 | if (spec == null) { |
@@ -93,7 +123,7 @@ public class AlarmRuleState { | @@ -93,7 +123,7 @@ public class AlarmRuleState { | ||
93 | } | 123 | } |
94 | } | 124 | } |
95 | 125 | ||
96 | - public boolean eval(DeviceDataSnapshot data) { | 126 | + public boolean eval(DataSnapshot data) { |
97 | boolean active = isActive(data.getTs()); | 127 | boolean active = isActive(data.getTs()); |
98 | switch (spec.getType()) { | 128 | switch (spec.getType()) { |
99 | case SIMPLE: | 129 | case SIMPLE: |
@@ -135,9 +165,7 @@ public class AlarmRuleState { | @@ -135,9 +165,7 @@ public class AlarmRuleState { | ||
135 | return false; | 165 | return false; |
136 | } | 166 | } |
137 | } | 167 | } |
138 | - long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli(); | ||
139 | - long msFromStartOfDay = eventTs - startOfDay; | ||
140 | - return schedule.getStartsOn() <= msFromStartOfDay && schedule.getEndsOn() > msFromStartOfDay; | 168 | + return isActive(eventTs, zoneId, zdt, schedule.getStartsOn(), schedule.getEndsOn()); |
141 | } | 169 | } |
142 | 170 | ||
143 | private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) { | 171 | private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) { |
@@ -147,9 +175,7 @@ public class AlarmRuleState { | @@ -147,9 +175,7 @@ public class AlarmRuleState { | ||
147 | for (CustomTimeScheduleItem item : schedule.getItems()) { | 175 | for (CustomTimeScheduleItem item : schedule.getItems()) { |
148 | if (item.getDayOfWeek() == dayOfWeek) { | 176 | if (item.getDayOfWeek() == dayOfWeek) { |
149 | if (item.isEnabled()) { | 177 | if (item.isEnabled()) { |
150 | - long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli(); | ||
151 | - long msFromStartOfDay = eventTs - startOfDay; | ||
152 | - return item.getStartsOn() <= msFromStartOfDay && item.getEndsOn() > msFromStartOfDay; | 178 | + return isActive(eventTs, zoneId, zdt, item.getStartsOn(), item.getEndsOn()); |
153 | } else { | 179 | } else { |
154 | return false; | 180 | return false; |
155 | } | 181 | } |
@@ -158,6 +184,16 @@ public class AlarmRuleState { | @@ -158,6 +184,16 @@ public class AlarmRuleState { | ||
158 | return false; | 184 | return false; |
159 | } | 185 | } |
160 | 186 | ||
187 | + private boolean isActive(long eventTs, ZoneId zoneId, ZonedDateTime zdt, long startsOn, long endsOn) { | ||
188 | + long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli(); | ||
189 | + long msFromStartOfDay = eventTs - startOfDay; | ||
190 | + if (startsOn <= endsOn) { | ||
191 | + return startsOn <= msFromStartOfDay && endsOn > msFromStartOfDay; | ||
192 | + } else { | ||
193 | + return startsOn < msFromStartOfDay || (0 < msFromStartOfDay && msFromStartOfDay < endsOn); | ||
194 | + } | ||
195 | + } | ||
196 | + | ||
161 | public void clear() { | 197 | public void clear() { |
162 | if (state.getEventCount() > 0 || state.getLastEventTs() > 0 || state.getDuration() > 0) { | 198 | if (state.getEventCount() > 0 || state.getLastEventTs() > 0 || state.getDuration() > 0) { |
163 | state.setEventCount(0L); | 199 | state.setEventCount(0L); |
@@ -167,7 +203,7 @@ public class AlarmRuleState { | @@ -167,7 +203,7 @@ public class AlarmRuleState { | ||
167 | } | 203 | } |
168 | } | 204 | } |
169 | 205 | ||
170 | - private boolean evalRepeating(DeviceDataSnapshot data, boolean active) { | 206 | + private boolean evalRepeating(DataSnapshot data, boolean active) { |
171 | if (active && eval(alarmRule.getCondition(), data)) { | 207 | if (active && eval(alarmRule.getCondition(), data)) { |
172 | state.setEventCount(state.getEventCount() + 1); | 208 | state.setEventCount(state.getEventCount() + 1); |
173 | updateFlag = true; | 209 | updateFlag = true; |
@@ -177,7 +213,7 @@ public class AlarmRuleState { | @@ -177,7 +213,7 @@ public class AlarmRuleState { | ||
177 | } | 213 | } |
178 | } | 214 | } |
179 | 215 | ||
180 | - private boolean evalDuration(DeviceDataSnapshot data, boolean active) { | 216 | + private boolean evalDuration(DataSnapshot data, boolean active) { |
181 | if (active && eval(alarmRule.getCondition(), data)) { | 217 | if (active && eval(alarmRule.getCondition(), data)) { |
182 | if (state.getLastEventTs() > 0) { | 218 | if (state.getLastEventTs() > 0) { |
183 | if (data.getTs() > state.getLastEventTs()) { | 219 | if (data.getTs() > state.getLastEventTs()) { |
@@ -211,45 +247,45 @@ public class AlarmRuleState { | @@ -211,45 +247,45 @@ public class AlarmRuleState { | ||
211 | } | 247 | } |
212 | } | 248 | } |
213 | 249 | ||
214 | - private boolean eval(AlarmCondition condition, DeviceDataSnapshot data) { | 250 | + private boolean eval(AlarmCondition condition, DataSnapshot data) { |
215 | boolean eval = true; | 251 | boolean eval = true; |
216 | for (KeyFilter keyFilter : condition.getCondition()) { | 252 | for (KeyFilter keyFilter : condition.getCondition()) { |
217 | EntityKeyValue value = data.getValue(keyFilter.getKey()); | 253 | EntityKeyValue value = data.getValue(keyFilter.getKey()); |
218 | if (value == null) { | 254 | if (value == null) { |
219 | return false; | 255 | return false; |
220 | } | 256 | } |
221 | - eval = eval && eval(value, keyFilter.getPredicate()); | 257 | + eval = eval && eval(data, value, keyFilter.getPredicate()); |
222 | } | 258 | } |
223 | return eval; | 259 | return eval; |
224 | } | 260 | } |
225 | 261 | ||
226 | - private boolean eval(EntityKeyValue value, KeyFilterPredicate predicate) { | 262 | + private boolean eval(DataSnapshot data, EntityKeyValue value, KeyFilterPredicate predicate) { |
227 | switch (predicate.getType()) { | 263 | switch (predicate.getType()) { |
228 | case STRING: | 264 | case STRING: |
229 | - return evalStrPredicate(value, (StringFilterPredicate) predicate); | 265 | + return evalStrPredicate(data, value, (StringFilterPredicate) predicate); |
230 | case NUMERIC: | 266 | case NUMERIC: |
231 | - return evalNumPredicate(value, (NumericFilterPredicate) predicate); | ||
232 | - case COMPLEX: | ||
233 | - return evalComplexPredicate(value, (ComplexFilterPredicate) predicate); | 267 | + return evalNumPredicate(data, value, (NumericFilterPredicate) predicate); |
234 | case BOOLEAN: | 268 | case BOOLEAN: |
235 | - return evalBoolPredicate(value, (BooleanFilterPredicate) predicate); | 269 | + return evalBoolPredicate(data, value, (BooleanFilterPredicate) predicate); |
270 | + case COMPLEX: | ||
271 | + return evalComplexPredicate(data, value, (ComplexFilterPredicate) predicate); | ||
236 | default: | 272 | default: |
237 | return false; | 273 | return false; |
238 | } | 274 | } |
239 | } | 275 | } |
240 | 276 | ||
241 | - private boolean evalComplexPredicate(EntityKeyValue ekv, ComplexFilterPredicate predicate) { | 277 | + private boolean evalComplexPredicate(DataSnapshot data, EntityKeyValue ekv, ComplexFilterPredicate predicate) { |
242 | switch (predicate.getOperation()) { | 278 | switch (predicate.getOperation()) { |
243 | case OR: | 279 | case OR: |
244 | for (KeyFilterPredicate kfp : predicate.getPredicates()) { | 280 | for (KeyFilterPredicate kfp : predicate.getPredicates()) { |
245 | - if (eval(ekv, kfp)) { | 281 | + if (eval(data, ekv, kfp)) { |
246 | return true; | 282 | return true; |
247 | } | 283 | } |
248 | } | 284 | } |
249 | return false; | 285 | return false; |
250 | case AND: | 286 | case AND: |
251 | for (KeyFilterPredicate kfp : predicate.getPredicates()) { | 287 | for (KeyFilterPredicate kfp : predicate.getPredicates()) { |
252 | - if (!eval(ekv, kfp)) { | 288 | + if (!eval(data, ekv, kfp)) { |
253 | return false; | 289 | return false; |
254 | } | 290 | } |
255 | } | 291 | } |
@@ -259,109 +295,55 @@ public class AlarmRuleState { | @@ -259,109 +295,55 @@ public class AlarmRuleState { | ||
259 | } | 295 | } |
260 | } | 296 | } |
261 | 297 | ||
262 | - private boolean evalBoolPredicate(EntityKeyValue ekv, BooleanFilterPredicate predicate) { | ||
263 | - Boolean value; | ||
264 | - switch (ekv.getDataType()) { | ||
265 | - case LONG: | ||
266 | - value = ekv.getLngValue() > 0; | ||
267 | - break; | ||
268 | - case DOUBLE: | ||
269 | - value = ekv.getDblValue() > 0; | ||
270 | - break; | ||
271 | - case BOOLEAN: | ||
272 | - value = ekv.getBoolValue(); | ||
273 | - break; | ||
274 | - case STRING: | ||
275 | - try { | ||
276 | - value = Boolean.parseBoolean(ekv.getStrValue()); | ||
277 | - break; | ||
278 | - } catch (RuntimeException e) { | ||
279 | - return false; | ||
280 | - } | ||
281 | - case JSON: | ||
282 | - try { | ||
283 | - value = Boolean.parseBoolean(ekv.getJsonValue()); | ||
284 | - break; | ||
285 | - } catch (RuntimeException e) { | ||
286 | - return false; | ||
287 | - } | ||
288 | - default: | ||
289 | - return false; | ||
290 | - } | ||
291 | - if (value == null) { | 298 | + private boolean evalBoolPredicate(DataSnapshot data, EntityKeyValue ekv, BooleanFilterPredicate predicate) { |
299 | + Boolean val = getBoolValue(ekv); | ||
300 | + if (val == null) { | ||
292 | return false; | 301 | return false; |
293 | } | 302 | } |
303 | + Boolean predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getBoolValue); | ||
294 | switch (predicate.getOperation()) { | 304 | switch (predicate.getOperation()) { |
295 | case EQUAL: | 305 | case EQUAL: |
296 | - return value.equals(predicate.getValue().getDefaultValue()); | 306 | + return val.equals(predicateValue); |
297 | case NOT_EQUAL: | 307 | case NOT_EQUAL: |
298 | - return !value.equals(predicate.getValue().getDefaultValue()); | 308 | + return !val.equals(predicateValue); |
299 | default: | 309 | default: |
300 | throw new RuntimeException("Operation not supported: " + predicate.getOperation()); | 310 | throw new RuntimeException("Operation not supported: " + predicate.getOperation()); |
301 | } | 311 | } |
302 | } | 312 | } |
303 | 313 | ||
304 | - private boolean evalNumPredicate(EntityKeyValue ekv, NumericFilterPredicate predicate) { | ||
305 | - Double value; | ||
306 | - switch (ekv.getDataType()) { | ||
307 | - case LONG: | ||
308 | - value = ekv.getLngValue().doubleValue(); | ||
309 | - break; | ||
310 | - case DOUBLE: | ||
311 | - value = ekv.getDblValue(); | ||
312 | - break; | ||
313 | - case BOOLEAN: | ||
314 | - value = ekv.getBoolValue() ? 1.0 : 0.0; | ||
315 | - break; | ||
316 | - case STRING: | ||
317 | - try { | ||
318 | - value = Double.parseDouble(ekv.getStrValue()); | ||
319 | - break; | ||
320 | - } catch (RuntimeException e) { | ||
321 | - return false; | ||
322 | - } | ||
323 | - case JSON: | ||
324 | - try { | ||
325 | - value = Double.parseDouble(ekv.getJsonValue()); | ||
326 | - break; | ||
327 | - } catch (RuntimeException e) { | ||
328 | - return false; | ||
329 | - } | ||
330 | - default: | ||
331 | - return false; | ||
332 | - } | ||
333 | - if (value == null) { | 314 | + private boolean evalNumPredicate(DataSnapshot data, EntityKeyValue ekv, NumericFilterPredicate predicate) { |
315 | + Double val = getDblValue(ekv); | ||
316 | + if (val == null) { | ||
334 | return false; | 317 | return false; |
335 | } | 318 | } |
336 | - | ||
337 | - Double predicateValue = predicate.getValue().getDefaultValue(); | 319 | + Double predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getDblValue); |
338 | switch (predicate.getOperation()) { | 320 | switch (predicate.getOperation()) { |
339 | case NOT_EQUAL: | 321 | case NOT_EQUAL: |
340 | - return !value.equals(predicateValue); | 322 | + return !val.equals(predicateValue); |
341 | case EQUAL: | 323 | case EQUAL: |
342 | - return value.equals(predicateValue); | 324 | + return val.equals(predicateValue); |
343 | case GREATER: | 325 | case GREATER: |
344 | - return value > predicateValue; | 326 | + return val > predicateValue; |
345 | case GREATER_OR_EQUAL: | 327 | case GREATER_OR_EQUAL: |
346 | - return value >= predicateValue; | 328 | + return val >= predicateValue; |
347 | case LESS: | 329 | case LESS: |
348 | - return value < predicateValue; | 330 | + return val < predicateValue; |
349 | case LESS_OR_EQUAL: | 331 | case LESS_OR_EQUAL: |
350 | - return value <= predicateValue; | 332 | + return val <= predicateValue; |
351 | default: | 333 | default: |
352 | throw new RuntimeException("Operation not supported: " + predicate.getOperation()); | 334 | throw new RuntimeException("Operation not supported: " + predicate.getOperation()); |
353 | } | 335 | } |
354 | } | 336 | } |
355 | 337 | ||
356 | - private boolean evalStrPredicate(EntityKeyValue ekv, StringFilterPredicate predicate) { | ||
357 | - String val; | ||
358 | - String predicateValue; | 338 | + private boolean evalStrPredicate(DataSnapshot data, EntityKeyValue ekv, StringFilterPredicate predicate) { |
339 | + String val = getStrValue(ekv); | ||
340 | + if (val == null) { | ||
341 | + return false; | ||
342 | + } | ||
343 | + String predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getStrValue); | ||
359 | if (predicate.isIgnoreCase()) { | 344 | if (predicate.isIgnoreCase()) { |
360 | - val = ekv.getStrValue().toLowerCase(); | ||
361 | - predicateValue = predicate.getValue().getDefaultValue().toLowerCase(); | ||
362 | - } else { | ||
363 | - val = ekv.getStrValue(); | ||
364 | - predicateValue = predicate.getValue().getDefaultValue(); | 345 | + val = val.toLowerCase(); |
346 | + predicateValue = predicateValue.toLowerCase(); | ||
365 | } | 347 | } |
366 | switch (predicate.getOperation()) { | 348 | switch (predicate.getOperation()) { |
367 | case CONTAINS: | 349 | case CONTAINS: |
@@ -380,4 +362,100 @@ public class AlarmRuleState { | @@ -380,4 +362,100 @@ public class AlarmRuleState { | ||
380 | throw new RuntimeException("Operation not supported: " + predicate.getOperation()); | 362 | throw new RuntimeException("Operation not supported: " + predicate.getOperation()); |
381 | } | 363 | } |
382 | } | 364 | } |
365 | + | ||
366 | + private <T> T getPredicateValue(DataSnapshot data, FilterPredicateValue<T> value, Function<EntityKeyValue, T> transformFunction) { | ||
367 | + EntityKeyValue ekv = getDynamicPredicateValue(data, value); | ||
368 | + if (ekv != null) { | ||
369 | + T result = transformFunction.apply(ekv); | ||
370 | + if (result != null) { | ||
371 | + return result; | ||
372 | + } | ||
373 | + } | ||
374 | + return value.getDefaultValue(); | ||
375 | + } | ||
376 | + | ||
377 | + private <T> EntityKeyValue getDynamicPredicateValue(DataSnapshot data, FilterPredicateValue<T> value) { | ||
378 | + EntityKeyValue ekv = null; | ||
379 | + if (value.getDynamicValue() != null) { | ||
380 | + ekv = data.getValue(new EntityKey(EntityKeyType.ATTRIBUTE, value.getDynamicValue().getSourceAttribute())); | ||
381 | + if (ekv == null) { | ||
382 | + ekv = data.getValue(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, value.getDynamicValue().getSourceAttribute())); | ||
383 | + if (ekv == null) { | ||
384 | + ekv = data.getValue(new EntityKey(EntityKeyType.SHARED_ATTRIBUTE, value.getDynamicValue().getSourceAttribute())); | ||
385 | + if (ekv == null) { | ||
386 | + ekv = data.getValue(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, value.getDynamicValue().getSourceAttribute())); | ||
387 | + } | ||
388 | + } | ||
389 | + } | ||
390 | + } | ||
391 | + return ekv; | ||
392 | + } | ||
393 | + | ||
394 | + private static String getStrValue(EntityKeyValue ekv) { | ||
395 | + switch (ekv.getDataType()) { | ||
396 | + case LONG: | ||
397 | + return ekv.getLngValue() != null ? ekv.getLngValue().toString() : null; | ||
398 | + case DOUBLE: | ||
399 | + return ekv.getDblValue() != null ? ekv.getDblValue().toString() : null; | ||
400 | + case BOOLEAN: | ||
401 | + return ekv.getBoolValue() != null ? ekv.getBoolValue().toString() : null; | ||
402 | + case STRING: | ||
403 | + return ekv.getStrValue(); | ||
404 | + case JSON: | ||
405 | + return ekv.getJsonValue(); | ||
406 | + default: | ||
407 | + return null; | ||
408 | + } | ||
409 | + } | ||
410 | + | ||
411 | + private static Double getDblValue(EntityKeyValue ekv) { | ||
412 | + switch (ekv.getDataType()) { | ||
413 | + case LONG: | ||
414 | + return ekv.getLngValue() != null ? ekv.getLngValue().doubleValue() : null; | ||
415 | + case DOUBLE: | ||
416 | + return ekv.getDblValue() != null ? ekv.getDblValue() : null; | ||
417 | + case BOOLEAN: | ||
418 | + return ekv.getBoolValue() != null ? (ekv.getBoolValue() ? 1.0 : 0.0) : null; | ||
419 | + case STRING: | ||
420 | + try { | ||
421 | + return Double.parseDouble(ekv.getStrValue()); | ||
422 | + } catch (RuntimeException e) { | ||
423 | + return null; | ||
424 | + } | ||
425 | + case JSON: | ||
426 | + try { | ||
427 | + return Double.parseDouble(ekv.getJsonValue()); | ||
428 | + } catch (RuntimeException e) { | ||
429 | + return null; | ||
430 | + } | ||
431 | + default: | ||
432 | + return null; | ||
433 | + } | ||
434 | + } | ||
435 | + | ||
436 | + private static Boolean getBoolValue(EntityKeyValue ekv) { | ||
437 | + switch (ekv.getDataType()) { | ||
438 | + case LONG: | ||
439 | + return ekv.getLngValue() != null ? ekv.getLngValue() > 0 : null; | ||
440 | + case DOUBLE: | ||
441 | + return ekv.getDblValue() != null ? ekv.getDblValue() > 0 : null; | ||
442 | + case BOOLEAN: | ||
443 | + return ekv.getBoolValue(); | ||
444 | + case STRING: | ||
445 | + try { | ||
446 | + return Boolean.parseBoolean(ekv.getStrValue()); | ||
447 | + } catch (RuntimeException e) { | ||
448 | + return null; | ||
449 | + } | ||
450 | + case JSON: | ||
451 | + try { | ||
452 | + return Boolean.parseBoolean(ekv.getJsonValue()); | ||
453 | + } catch (RuntimeException e) { | ||
454 | + return null; | ||
455 | + } | ||
456 | + default: | ||
457 | + return null; | ||
458 | + } | ||
459 | + } | ||
460 | + | ||
383 | } | 461 | } |
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java
renamed from
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileAlarmState.java
@@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.profile; | @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.profile; | ||
17 | 17 | ||
18 | import com.fasterxml.jackson.databind.JsonNode; | 18 | import com.fasterxml.jackson.databind.JsonNode; |
19 | import lombok.Data; | 19 | import lombok.Data; |
20 | +import lombok.extern.slf4j.Slf4j; | ||
20 | import org.thingsboard.rule.engine.action.TbAlarmResult; | 21 | import org.thingsboard.rule.engine.action.TbAlarmResult; |
21 | import org.thingsboard.rule.engine.api.TbContext; | 22 | import org.thingsboard.rule.engine.api.TbContext; |
22 | import org.thingsboard.rule.engine.profile.state.PersistedAlarmRuleState; | 23 | import org.thingsboard.rule.engine.profile.state.PersistedAlarmRuleState; |
@@ -27,6 +28,7 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity; | @@ -27,6 +28,7 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity; | ||
27 | import org.thingsboard.server.common.data.alarm.AlarmStatus; | 28 | import org.thingsboard.server.common.data.alarm.AlarmStatus; |
28 | import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; | 29 | import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; |
29 | import org.thingsboard.server.common.data.id.EntityId; | 30 | import org.thingsboard.server.common.data.id.EntityId; |
31 | +import org.thingsboard.server.common.data.query.EntityKeyType; | ||
30 | import org.thingsboard.server.common.msg.TbMsg; | 32 | import org.thingsboard.server.common.msg.TbMsg; |
31 | import org.thingsboard.server.common.msg.TbMsgMetaData; | 33 | import org.thingsboard.server.common.msg.TbMsgMetaData; |
32 | import org.thingsboard.server.common.msg.queue.ServiceQueue; | 34 | import org.thingsboard.server.common.msg.queue.ServiceQueue; |
@@ -39,8 +41,10 @@ import java.util.concurrent.ExecutionException; | @@ -39,8 +41,10 @@ import java.util.concurrent.ExecutionException; | ||
39 | import java.util.function.BiFunction; | 41 | import java.util.function.BiFunction; |
40 | 42 | ||
41 | @Data | 43 | @Data |
42 | -class DeviceProfileAlarmState { | 44 | +@Slf4j |
45 | +class AlarmState { | ||
43 | 46 | ||
47 | + private final ProfileState deviceProfile; | ||
44 | private final EntityId originator; | 48 | private final EntityId originator; |
45 | private DeviceProfileAlarm alarmDefinition; | 49 | private DeviceProfileAlarm alarmDefinition; |
46 | private volatile List<AlarmRuleState> createRulesSortedBySeverityDesc; | 50 | private volatile List<AlarmRuleState> createRulesSortedBySeverityDesc; |
@@ -50,27 +54,33 @@ class DeviceProfileAlarmState { | @@ -50,27 +54,33 @@ class DeviceProfileAlarmState { | ||
50 | private volatile TbMsgMetaData lastMsgMetaData; | 54 | private volatile TbMsgMetaData lastMsgMetaData; |
51 | private volatile String lastMsgQueueName; | 55 | private volatile String lastMsgQueueName; |
52 | 56 | ||
53 | - public DeviceProfileAlarmState(EntityId originator, DeviceProfileAlarm alarmDefinition, PersistedAlarmState alarmState) { | 57 | + AlarmState(ProfileState deviceProfile, EntityId originator, DeviceProfileAlarm alarmDefinition, PersistedAlarmState alarmState) { |
58 | + this.deviceProfile = deviceProfile; | ||
54 | this.originator = originator; | 59 | this.originator = originator; |
55 | this.updateState(alarmDefinition, alarmState); | 60 | this.updateState(alarmDefinition, alarmState); |
56 | } | 61 | } |
57 | 62 | ||
58 | - public boolean process(TbContext ctx, TbMsg msg, DeviceDataSnapshot data) throws ExecutionException, InterruptedException { | 63 | + public boolean process(TbContext ctx, TbMsg msg, DataSnapshot data, SnapshotUpdate update) throws ExecutionException, InterruptedException { |
59 | initCurrentAlarm(ctx); | 64 | initCurrentAlarm(ctx); |
60 | lastMsgMetaData = msg.getMetaData(); | 65 | lastMsgMetaData = msg.getMetaData(); |
61 | lastMsgQueueName = msg.getQueueName(); | 66 | lastMsgQueueName = msg.getQueueName(); |
62 | - return createOrClearAlarms(ctx, data, AlarmRuleState::eval); | 67 | + return createOrClearAlarms(ctx, data, update, AlarmRuleState::eval); |
63 | } | 68 | } |
64 | 69 | ||
65 | public boolean process(TbContext ctx, long ts) throws ExecutionException, InterruptedException { | 70 | public boolean process(TbContext ctx, long ts) throws ExecutionException, InterruptedException { |
66 | initCurrentAlarm(ctx); | 71 | initCurrentAlarm(ctx); |
67 | - return createOrClearAlarms(ctx, ts, AlarmRuleState::eval); | 72 | + return createOrClearAlarms(ctx, ts, null, AlarmRuleState::eval); |
68 | } | 73 | } |
69 | 74 | ||
70 | - public <T> boolean createOrClearAlarms(TbContext ctx, T data, BiFunction<AlarmRuleState, T, Boolean> evalFunction) { | 75 | + public <T> boolean createOrClearAlarms(TbContext ctx, T data, SnapshotUpdate update, BiFunction<AlarmRuleState, T, Boolean> evalFunction) { |
71 | boolean stateUpdate = false; | 76 | boolean stateUpdate = false; |
72 | AlarmSeverity resultSeverity = null; | 77 | AlarmSeverity resultSeverity = null; |
78 | + log.debug("[{}] processing update: {}", alarmDefinition.getId(), data); | ||
73 | for (AlarmRuleState state : createRulesSortedBySeverityDesc) { | 79 | for (AlarmRuleState state : createRulesSortedBySeverityDesc) { |
80 | + if (!validateUpdate(update, state)) { | ||
81 | + log.debug("[{}][{}] Update is not valid for current rule state", alarmDefinition.getId(), state.getSeverity()); | ||
82 | + continue; | ||
83 | + } | ||
74 | boolean evalResult = evalFunction.apply(state, data); | 84 | boolean evalResult = evalFunction.apply(state, data); |
75 | stateUpdate |= state.checkUpdate(); | 85 | stateUpdate |= state.checkUpdate(); |
76 | if (evalResult) { | 86 | if (evalResult) { |
@@ -81,9 +91,17 @@ class DeviceProfileAlarmState { | @@ -81,9 +91,17 @@ class DeviceProfileAlarmState { | ||
81 | if (resultSeverity != null) { | 91 | if (resultSeverity != null) { |
82 | pushMsg(ctx, calculateAlarmResult(ctx, resultSeverity)); | 92 | pushMsg(ctx, calculateAlarmResult(ctx, resultSeverity)); |
83 | } else if (currentAlarm != null && clearState != null) { | 93 | } else if (currentAlarm != null && clearState != null) { |
94 | + if (!validateUpdate(update, clearState)) { | ||
95 | + log.debug("[{}] Update is not valid for current clear state", alarmDefinition.getId()); | ||
96 | + return stateUpdate; | ||
97 | + } | ||
84 | Boolean evalResult = evalFunction.apply(clearState, data); | 98 | Boolean evalResult = evalFunction.apply(clearState, data); |
85 | if (evalResult) { | 99 | if (evalResult) { |
86 | stateUpdate |= clearState.checkUpdate(); | 100 | stateUpdate |= clearState.checkUpdate(); |
101 | + for (AlarmRuleState state : createRulesSortedBySeverityDesc) { | ||
102 | + state.clear(); | ||
103 | + stateUpdate |= state.checkUpdate(); | ||
104 | + } | ||
87 | ctx.getAlarmService().clearAlarm(ctx.getTenantId(), currentAlarm.getId(), JacksonUtil.OBJECT_MAPPER.createObjectNode(), System.currentTimeMillis()); | 105 | ctx.getAlarmService().clearAlarm(ctx.getTenantId(), currentAlarm.getId(), JacksonUtil.OBJECT_MAPPER.createObjectNode(), System.currentTimeMillis()); |
88 | pushMsg(ctx, new TbAlarmResult(false, false, true, currentAlarm)); | 106 | pushMsg(ctx, new TbAlarmResult(false, false, true, currentAlarm)); |
89 | currentAlarm = null; | 107 | currentAlarm = null; |
@@ -92,6 +110,18 @@ class DeviceProfileAlarmState { | @@ -92,6 +110,18 @@ class DeviceProfileAlarmState { | ||
92 | return stateUpdate; | 110 | return stateUpdate; |
93 | } | 111 | } |
94 | 112 | ||
113 | + public boolean validateUpdate(SnapshotUpdate update, AlarmRuleState state) { | ||
114 | + if (update != null) { | ||
115 | + //Check that the update type and that keys match. | ||
116 | + if (update.getType().equals(EntityKeyType.TIME_SERIES)) { | ||
117 | + return state.validateTsUpdate(update.getKeys()); | ||
118 | + } else if (update.getType().equals(EntityKeyType.ATTRIBUTE)) { | ||
119 | + return state.validateAttrUpdate(update.getKeys()); | ||
120 | + } | ||
121 | + } | ||
122 | + return true; | ||
123 | + } | ||
124 | + | ||
95 | public void initCurrentAlarm(TbContext ctx) throws InterruptedException, ExecutionException { | 125 | public void initCurrentAlarm(TbContext ctx) throws InterruptedException, ExecutionException { |
96 | if (!initialFetchDone) { | 126 | if (!initialFetchDone) { |
97 | Alarm alarm = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), originator, alarmDefinition.getAlarmType()).get(); | 127 | Alarm alarm = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), originator, alarmDefinition.getAlarmType()).get(); |
@@ -137,17 +167,20 @@ class DeviceProfileAlarmState { | @@ -137,17 +167,20 @@ class DeviceProfileAlarmState { | ||
137 | alarmState.getCreateRuleStates().put(severity, ruleState); | 167 | alarmState.getCreateRuleStates().put(severity, ruleState); |
138 | } | 168 | } |
139 | } | 169 | } |
140 | - createRulesSortedBySeverityDesc.add(new AlarmRuleState(severity, rule, ruleState)); | 170 | + createRulesSortedBySeverityDesc.add(new AlarmRuleState(severity, rule, |
171 | + deviceProfile.getCreateAlarmKeys(alarm.getId(), severity), ruleState)); | ||
141 | }); | 172 | }); |
142 | createRulesSortedBySeverityDesc.sort(Comparator.comparingInt(state -> state.getSeverity().ordinal())); | 173 | createRulesSortedBySeverityDesc.sort(Comparator.comparingInt(state -> state.getSeverity().ordinal())); |
143 | PersistedAlarmRuleState ruleState = alarmState == null ? null : alarmState.getClearRuleState(); | 174 | PersistedAlarmRuleState ruleState = alarmState == null ? null : alarmState.getClearRuleState(); |
144 | if (alarmDefinition.getClearRule() != null) { | 175 | if (alarmDefinition.getClearRule() != null) { |
145 | - clearState = new AlarmRuleState(null, alarmDefinition.getClearRule(), ruleState); | 176 | + clearState = new AlarmRuleState(null, alarmDefinition.getClearRule(), deviceProfile.getClearAlarmKeys(alarm.getId()), ruleState); |
146 | } | 177 | } |
147 | } | 178 | } |
148 | 179 | ||
149 | private TbAlarmResult calculateAlarmResult(TbContext ctx, AlarmSeverity severity) { | 180 | private TbAlarmResult calculateAlarmResult(TbContext ctx, AlarmSeverity severity) { |
150 | if (currentAlarm != null) { | 181 | if (currentAlarm != null) { |
182 | + // TODO: In some extremely rare cases, we might miss the event of alarm clear (If one use in-mem queue and restarted the server) or (if one manipulated the rule chain). | ||
183 | + // Maybe we should fetch alarm every time? | ||
151 | currentAlarm.setEndTs(System.currentTimeMillis()); | 184 | currentAlarm.setEndTs(System.currentTimeMillis()); |
152 | AlarmSeverity oldSeverity = currentAlarm.getSeverity(); | 185 | AlarmSeverity oldSeverity = currentAlarm.getSeverity(); |
153 | if (!oldSeverity.equals(severity)) { | 186 | if (!oldSeverity.equals(severity)) { |
@@ -15,7 +15,7 @@ | @@ -15,7 +15,7 @@ | ||
15 | */ | 15 | */ |
16 | package org.thingsboard.rule.engine.profile; | 16 | package org.thingsboard.rule.engine.profile; |
17 | 17 | ||
18 | -public enum AlarmStateUpdateResult { | 18 | +enum AlarmStateUpdateResult { |
19 | 19 | ||
20 | NONE, CREATED, UPDATED, SEVERITY_UPDATED, CLEARED; | 20 | NONE, CREATED, UPDATED, SEVERITY_UPDATED, CLEARED; |
21 | 21 |
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java
renamed from
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceDataSnapshot.java
@@ -24,7 +24,7 @@ import java.util.Map; | @@ -24,7 +24,7 @@ import java.util.Map; | ||
24 | import java.util.Set; | 24 | import java.util.Set; |
25 | import java.util.concurrent.ConcurrentHashMap; | 25 | import java.util.concurrent.ConcurrentHashMap; |
26 | 26 | ||
27 | -public class DeviceDataSnapshot { | 27 | +class DataSnapshot { |
28 | 28 | ||
29 | private volatile boolean ready; | 29 | private volatile boolean ready; |
30 | @Getter | 30 | @Getter |
@@ -33,7 +33,7 @@ public class DeviceDataSnapshot { | @@ -33,7 +33,7 @@ public class DeviceDataSnapshot { | ||
33 | private final Set<EntityKey> keys; | 33 | private final Set<EntityKey> keys; |
34 | private final Map<EntityKey, EntityKeyValue> values = new ConcurrentHashMap<>(); | 34 | private final Map<EntityKey, EntityKeyValue> values = new ConcurrentHashMap<>(); |
35 | 35 | ||
36 | - public DeviceDataSnapshot(Set<EntityKey> entityKeysToFetch) { | 36 | + DataSnapshot(Set<EntityKey> entityKeysToFetch) { |
37 | this.keys = entityKeysToFetch; | 37 | this.keys = entityKeysToFetch; |
38 | } | 38 | } |
39 | 39 | ||
@@ -56,28 +56,38 @@ public class DeviceDataSnapshot { | @@ -56,28 +56,38 @@ public class DeviceDataSnapshot { | ||
56 | } | 56 | } |
57 | } | 57 | } |
58 | 58 | ||
59 | - void putValue(EntityKey key, EntityKeyValue value) { | 59 | + boolean putValue(EntityKey key, long newTs, EntityKeyValue value) { |
60 | + boolean updateOfTs = ts != newTs; | ||
61 | + boolean result = false; | ||
60 | switch (key.getType()) { | 62 | switch (key.getType()) { |
61 | case ATTRIBUTE: | 63 | case ATTRIBUTE: |
62 | - putIfKeyExists(key, value); | ||
63 | - putIfKeyExists(getAttrKey(key, EntityKeyType.CLIENT_ATTRIBUTE), value); | ||
64 | - putIfKeyExists(getAttrKey(key, EntityKeyType.SHARED_ATTRIBUTE), value); | ||
65 | - putIfKeyExists(getAttrKey(key, EntityKeyType.SERVER_ATTRIBUTE), value); | 64 | + result |= putIfKeyExists(key, value, updateOfTs); |
65 | + result |= putIfKeyExists(getAttrKey(key, EntityKeyType.CLIENT_ATTRIBUTE), value, updateOfTs); | ||
66 | + result |= putIfKeyExists(getAttrKey(key, EntityKeyType.SHARED_ATTRIBUTE), value, updateOfTs); | ||
67 | + result |= putIfKeyExists(getAttrKey(key, EntityKeyType.SERVER_ATTRIBUTE), value, updateOfTs); | ||
66 | break; | 68 | break; |
67 | case CLIENT_ATTRIBUTE: | 69 | case CLIENT_ATTRIBUTE: |
68 | case SHARED_ATTRIBUTE: | 70 | case SHARED_ATTRIBUTE: |
69 | case SERVER_ATTRIBUTE: | 71 | case SERVER_ATTRIBUTE: |
70 | - putIfKeyExists(key, value); | ||
71 | - putIfKeyExists(getAttrKey(key, EntityKeyType.ATTRIBUTE), value); | 72 | + result |= putIfKeyExists(key, value, updateOfTs); |
73 | + result |= putIfKeyExists(getAttrKey(key, EntityKeyType.ATTRIBUTE), value, updateOfTs); | ||
72 | break; | 74 | break; |
73 | default: | 75 | default: |
74 | - putIfKeyExists(key, value); | 76 | + result |= putIfKeyExists(key, value, updateOfTs); |
75 | } | 77 | } |
78 | + return result; | ||
76 | } | 79 | } |
77 | 80 | ||
78 | - private void putIfKeyExists(EntityKey key, EntityKeyValue value) { | 81 | + private boolean putIfKeyExists(EntityKey key, EntityKeyValue value, boolean updateOfTs) { |
79 | if (keys.contains(key)) { | 82 | if (keys.contains(key)) { |
80 | - values.put(key, value); | 83 | + EntityKeyValue oldValue = values.put(key, value); |
84 | + if (updateOfTs) { | ||
85 | + return true; | ||
86 | + } else { | ||
87 | + return oldValue == null || !oldValue.equals(value); | ||
88 | + } | ||
89 | + } else { | ||
90 | + return false; | ||
81 | } | 91 | } |
82 | } | 92 | } |
83 | 93 |
@@ -16,6 +16,7 @@ | @@ -16,6 +16,7 @@ | ||
16 | package org.thingsboard.rule.engine.profile; | 16 | package org.thingsboard.rule.engine.profile; |
17 | 17 | ||
18 | import com.google.gson.JsonParser; | 18 | import com.google.gson.JsonParser; |
19 | +import lombok.extern.slf4j.Slf4j; | ||
19 | import org.springframework.util.StringUtils; | 20 | import org.springframework.util.StringUtils; |
20 | import org.thingsboard.rule.engine.api.TbContext; | 21 | import org.thingsboard.rule.engine.api.TbContext; |
21 | import org.thingsboard.rule.engine.profile.state.PersistedAlarmState; | 22 | import org.thingsboard.rule.engine.profile.state.PersistedAlarmState; |
@@ -29,7 +30,6 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; | @@ -29,7 +30,6 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; | ||
29 | import org.thingsboard.server.common.data.id.DeviceId; | 30 | import org.thingsboard.server.common.data.id.DeviceId; |
30 | import org.thingsboard.server.common.data.id.DeviceProfileId; | 31 | import org.thingsboard.server.common.data.id.DeviceProfileId; |
31 | import org.thingsboard.server.common.data.id.EntityId; | 32 | import org.thingsboard.server.common.data.id.EntityId; |
32 | -import org.thingsboard.server.common.data.id.RuleNodeStateId; | ||
33 | import org.thingsboard.server.common.data.kv.AttributeKvEntry; | 33 | import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
34 | import org.thingsboard.server.common.data.kv.KvEntry; | 34 | import org.thingsboard.server.common.data.kv.KvEntry; |
35 | import org.thingsboard.server.common.data.kv.TsKvEntry; | 35 | import org.thingsboard.server.common.data.kv.TsKvEntry; |
@@ -53,17 +53,18 @@ import java.util.concurrent.ConcurrentMap; | @@ -53,17 +53,18 @@ import java.util.concurrent.ConcurrentMap; | ||
53 | import java.util.concurrent.ExecutionException; | 53 | import java.util.concurrent.ExecutionException; |
54 | import java.util.stream.Collectors; | 54 | import java.util.stream.Collectors; |
55 | 55 | ||
56 | +@Slf4j | ||
56 | class DeviceState { | 57 | class DeviceState { |
57 | 58 | ||
58 | private final boolean persistState; | 59 | private final boolean persistState; |
59 | private final DeviceId deviceId; | 60 | private final DeviceId deviceId; |
61 | + private final ProfileState deviceProfile; | ||
60 | private RuleNodeState state; | 62 | private RuleNodeState state; |
61 | - private DeviceProfileState deviceProfile; | ||
62 | private PersistedDeviceState pds; | 63 | private PersistedDeviceState pds; |
63 | - private DeviceDataSnapshot latestValues; | ||
64 | - private final ConcurrentMap<String, DeviceProfileAlarmState> alarmStates = new ConcurrentHashMap<>(); | 64 | + private DataSnapshot latestValues; |
65 | + private final ConcurrentMap<String, AlarmState> alarmStates = new ConcurrentHashMap<>(); | ||
65 | 66 | ||
66 | - public DeviceState(TbContext ctx, TbDeviceProfileNodeConfiguration config, DeviceId deviceId, DeviceProfileState deviceProfile, RuleNodeState state) { | 67 | + DeviceState(TbContext ctx, TbDeviceProfileNodeConfiguration config, DeviceId deviceId, ProfileState deviceProfile, RuleNodeState state) { |
67 | this.persistState = config.isPersistAlarmRulesState(); | 68 | this.persistState = config.isPersistAlarmRulesState(); |
68 | this.deviceId = deviceId; | 69 | this.deviceId = deviceId; |
69 | this.deviceProfile = deviceProfile; | 70 | this.deviceProfile = deviceProfile; |
@@ -86,7 +87,7 @@ class DeviceState { | @@ -86,7 +87,7 @@ class DeviceState { | ||
86 | if (pds != null) { | 87 | if (pds != null) { |
87 | for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { | 88 | for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { |
88 | alarmStates.computeIfAbsent(alarm.getId(), | 89 | alarmStates.computeIfAbsent(alarm.getId(), |
89 | - a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); | 90 | + a -> new AlarmState(deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); |
90 | } | 91 | } |
91 | } | 92 | } |
92 | } | 93 | } |
@@ -107,14 +108,20 @@ class DeviceState { | @@ -107,14 +108,20 @@ class DeviceState { | ||
107 | if (alarmStates.containsKey(alarm.getId())) { | 108 | if (alarmStates.containsKey(alarm.getId())) { |
108 | alarmStates.get(alarm.getId()).updateState(alarm, getOrInitPersistedAlarmState(alarm)); | 109 | alarmStates.get(alarm.getId()).updateState(alarm, getOrInitPersistedAlarmState(alarm)); |
109 | } else { | 110 | } else { |
110 | - alarmStates.putIfAbsent(alarm.getId(), new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); | 111 | + alarmStates.putIfAbsent(alarm.getId(), new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); |
111 | } | 112 | } |
112 | } | 113 | } |
113 | } | 114 | } |
114 | 115 | ||
115 | public void harvestAlarms(TbContext ctx, long ts) throws ExecutionException, InterruptedException { | 116 | public void harvestAlarms(TbContext ctx, long ts) throws ExecutionException, InterruptedException { |
116 | - for (DeviceProfileAlarmState state : alarmStates.values()) { | ||
117 | - state.process(ctx, ts); | 117 | + log.debug("[{}] Going to harvest alarms: {}", ctx.getSelfId(), ts); |
118 | + boolean stateChanged = false; | ||
119 | + for (AlarmState state : alarmStates.values()) { | ||
120 | + stateChanged |= state.process(ctx, ts); | ||
121 | + } | ||
122 | + if (persistState && stateChanged) { | ||
123 | + state.setStateData(JacksonUtil.toString(pds)); | ||
124 | + state = ctx.saveRuleNodeState(state); | ||
118 | } | 125 | } |
119 | } | 126 | } |
120 | 127 | ||
@@ -146,8 +153,8 @@ class DeviceState { | @@ -146,8 +153,8 @@ class DeviceState { | ||
146 | boolean stateChanged = false; | 153 | boolean stateChanged = false; |
147 | Alarm alarmNf = JacksonUtil.fromString(msg.getData(), Alarm.class); | 154 | Alarm alarmNf = JacksonUtil.fromString(msg.getData(), Alarm.class); |
148 | for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { | 155 | for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { |
149 | - DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), | ||
150 | - a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); | 156 | + AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), |
157 | + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); | ||
151 | stateChanged |= alarmState.processAlarmClear(ctx, alarmNf); | 158 | stateChanged |= alarmState.processAlarmClear(ctx, alarmNf); |
152 | } | 159 | } |
153 | ctx.tellSuccess(msg); | 160 | ctx.tellSuccess(msg); |
@@ -175,9 +182,9 @@ class DeviceState { | @@ -175,9 +182,9 @@ class DeviceState { | ||
175 | EntityKeyType keyType = getKeyTypeFromScope(scope); | 182 | EntityKeyType keyType = getKeyTypeFromScope(scope); |
176 | keys.forEach(key -> latestValues.removeValue(new EntityKey(keyType, key))); | 183 | keys.forEach(key -> latestValues.removeValue(new EntityKey(keyType, key))); |
177 | for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { | 184 | for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { |
178 | - DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), | ||
179 | - a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); | ||
180 | - stateChanged |= alarmState.process(ctx, msg, latestValues); | 185 | + AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), |
186 | + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); | ||
187 | + stateChanged |= alarmState.process(ctx, msg, latestValues, null); | ||
181 | } | 188 | } |
182 | } | 189 | } |
183 | ctx.tellSuccess(msg); | 190 | ctx.tellSuccess(msg); |
@@ -192,11 +199,11 @@ class DeviceState { | @@ -192,11 +199,11 @@ class DeviceState { | ||
192 | private boolean processAttributesUpdate(TbContext ctx, TbMsg msg, Set<AttributeKvEntry> attributes, String scope) throws ExecutionException, InterruptedException { | 199 | private boolean processAttributesUpdate(TbContext ctx, TbMsg msg, Set<AttributeKvEntry> attributes, String scope) throws ExecutionException, InterruptedException { |
193 | boolean stateChanged = false; | 200 | boolean stateChanged = false; |
194 | if (!attributes.isEmpty()) { | 201 | if (!attributes.isEmpty()) { |
195 | - merge(latestValues, attributes, scope); | 202 | + SnapshotUpdate update = merge(latestValues, attributes, scope); |
196 | for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { | 203 | for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { |
197 | - DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), | ||
198 | - a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); | ||
199 | - stateChanged |= alarmState.process(ctx, msg, latestValues); | 204 | + AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), |
205 | + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); | ||
206 | + stateChanged |= alarmState.process(ctx, msg, latestValues, update); | ||
200 | } | 207 | } |
201 | } | 208 | } |
202 | ctx.tellSuccess(msg); | 209 | ctx.tellSuccess(msg); |
@@ -206,34 +213,47 @@ class DeviceState { | @@ -206,34 +213,47 @@ class DeviceState { | ||
206 | protected boolean processTelemetry(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { | 213 | protected boolean processTelemetry(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { |
207 | boolean stateChanged = false; | 214 | boolean stateChanged = false; |
208 | Map<Long, List<KvEntry>> tsKvMap = JsonConverter.convertToSortedTelemetry(new JsonParser().parse(msg.getData()), TbMsgTimeseriesNode.getTs(msg)); | 215 | Map<Long, List<KvEntry>> tsKvMap = JsonConverter.convertToSortedTelemetry(new JsonParser().parse(msg.getData()), TbMsgTimeseriesNode.getTs(msg)); |
216 | + // iterate over data by ts (ASC order). | ||
209 | for (Map.Entry<Long, List<KvEntry>> entry : tsKvMap.entrySet()) { | 217 | for (Map.Entry<Long, List<KvEntry>> entry : tsKvMap.entrySet()) { |
210 | Long ts = entry.getKey(); | 218 | Long ts = entry.getKey(); |
211 | List<KvEntry> data = entry.getValue(); | 219 | List<KvEntry> data = entry.getValue(); |
212 | - merge(latestValues, ts, data); | ||
213 | - for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { | ||
214 | - DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), | ||
215 | - a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); | ||
216 | - stateChanged |= alarmState.process(ctx, msg, latestValues); | 220 | + SnapshotUpdate update = merge(latestValues, ts, data); |
221 | + if (update.hasUpdate()) { | ||
222 | + for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { | ||
223 | + AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), | ||
224 | + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); | ||
225 | + stateChanged |= alarmState.process(ctx, msg, latestValues, update); | ||
226 | + } | ||
217 | } | 227 | } |
218 | } | 228 | } |
219 | ctx.tellSuccess(msg); | 229 | ctx.tellSuccess(msg); |
220 | return stateChanged; | 230 | return stateChanged; |
221 | } | 231 | } |
222 | 232 | ||
223 | - private void merge(DeviceDataSnapshot latestValues, Long ts, List<KvEntry> data) { | ||
224 | - latestValues.setTs(ts); | 233 | + private SnapshotUpdate merge(DataSnapshot latestValues, Long newTs, List<KvEntry> data) { |
234 | + Set<EntityKey> keys = new HashSet<>(); | ||
225 | for (KvEntry entry : data) { | 235 | for (KvEntry entry : data) { |
226 | - latestValues.putValue(new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()), toEntityValue(entry)); | 236 | + EntityKey entityKey = new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()); |
237 | + if (latestValues.putValue(entityKey, newTs, toEntityValue(entry))) { | ||
238 | + keys.add(entityKey); | ||
239 | + } | ||
227 | } | 240 | } |
241 | + latestValues.setTs(newTs); | ||
242 | + return new SnapshotUpdate(EntityKeyType.TIME_SERIES, keys); | ||
228 | } | 243 | } |
229 | 244 | ||
230 | - private void merge(DeviceDataSnapshot latestValues, Set<AttributeKvEntry> attributes, String scope) { | ||
231 | - long ts = latestValues.getTs(); | 245 | + private SnapshotUpdate merge(DataSnapshot latestValues, Set<AttributeKvEntry> attributes, String scope) { |
246 | + long newTs = 0; | ||
247 | + Set<EntityKey> keys = new HashSet<>(); | ||
232 | for (AttributeKvEntry entry : attributes) { | 248 | for (AttributeKvEntry entry : attributes) { |
233 | - ts = Math.max(ts, entry.getLastUpdateTs()); | ||
234 | - latestValues.putValue(new EntityKey(getKeyTypeFromScope(scope), entry.getKey()), toEntityValue(entry)); | 249 | + newTs = Math.max(newTs, entry.getLastUpdateTs()); |
250 | + EntityKey entityKey = new EntityKey(getKeyTypeFromScope(scope), entry.getKey()); | ||
251 | + if (latestValues.putValue(entityKey, newTs, toEntityValue(entry))) { | ||
252 | + keys.add(entityKey); | ||
253 | + } | ||
235 | } | 254 | } |
236 | - latestValues.setTs(ts); | 255 | + latestValues.setTs(newTs); |
256 | + return new SnapshotUpdate(EntityKeyType.ATTRIBUTE, keys); | ||
237 | } | 257 | } |
238 | 258 | ||
239 | private static EntityKeyType getKeyTypeFromScope(String scope) { | 259 | private static EntityKeyType getKeyTypeFromScope(String scope) { |
@@ -248,14 +268,14 @@ class DeviceState { | @@ -248,14 +268,14 @@ class DeviceState { | ||
248 | return EntityKeyType.ATTRIBUTE; | 268 | return EntityKeyType.ATTRIBUTE; |
249 | } | 269 | } |
250 | 270 | ||
251 | - private DeviceDataSnapshot fetchLatestValues(TbContext ctx, EntityId originator) throws ExecutionException, InterruptedException { | 271 | + private DataSnapshot fetchLatestValues(TbContext ctx, EntityId originator) throws ExecutionException, InterruptedException { |
252 | Set<EntityKey> entityKeysToFetch = deviceProfile.getEntityKeys(); | 272 | Set<EntityKey> entityKeysToFetch = deviceProfile.getEntityKeys(); |
253 | - DeviceDataSnapshot result = new DeviceDataSnapshot(entityKeysToFetch); | 273 | + DataSnapshot result = new DataSnapshot(entityKeysToFetch); |
254 | addEntityKeysToSnapshot(ctx, originator, entityKeysToFetch, result); | 274 | addEntityKeysToSnapshot(ctx, originator, entityKeysToFetch, result); |
255 | return result; | 275 | return result; |
256 | } | 276 | } |
257 | 277 | ||
258 | - private void addEntityKeysToSnapshot(TbContext ctx, EntityId originator, Set<EntityKey> entityKeysToFetch, DeviceDataSnapshot result) throws InterruptedException, ExecutionException { | 278 | + private void addEntityKeysToSnapshot(TbContext ctx, EntityId originator, Set<EntityKey> entityKeysToFetch, DataSnapshot result) throws InterruptedException, ExecutionException { |
259 | Set<String> serverAttributeKeys = new HashSet<>(); | 279 | Set<String> serverAttributeKeys = new HashSet<>(); |
260 | Set<String> clientAttributeKeys = new HashSet<>(); | 280 | Set<String> clientAttributeKeys = new HashSet<>(); |
261 | Set<String> sharedAttributeKeys = new HashSet<>(); | 281 | Set<String> sharedAttributeKeys = new HashSet<>(); |
@@ -291,16 +311,16 @@ class DeviceState { | @@ -291,16 +311,16 @@ class DeviceState { | ||
291 | if (device != null) { | 311 | if (device != null) { |
292 | switch (key) { | 312 | switch (key) { |
293 | case EntityKeyMapping.NAME: | 313 | case EntityKeyMapping.NAME: |
294 | - result.putValue(entityKey, EntityKeyValue.fromString(device.getName())); | 314 | + result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromString(device.getName())); |
295 | break; | 315 | break; |
296 | case EntityKeyMapping.TYPE: | 316 | case EntityKeyMapping.TYPE: |
297 | - result.putValue(entityKey, EntityKeyValue.fromString(device.getType())); | 317 | + result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromString(device.getType())); |
298 | break; | 318 | break; |
299 | case EntityKeyMapping.CREATED_TIME: | 319 | case EntityKeyMapping.CREATED_TIME: |
300 | - result.putValue(entityKey, EntityKeyValue.fromLong(device.getCreatedTime())); | 320 | + result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromLong(device.getCreatedTime())); |
301 | break; | 321 | break; |
302 | case EntityKeyMapping.LABEL: | 322 | case EntityKeyMapping.LABEL: |
303 | - result.putValue(entityKey, EntityKeyValue.fromString(device.getLabel())); | 323 | + result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromString(device.getLabel())); |
304 | break; | 324 | break; |
305 | } | 325 | } |
306 | } | 326 | } |
@@ -312,7 +332,7 @@ class DeviceState { | @@ -312,7 +332,7 @@ class DeviceState { | ||
312 | List<TsKvEntry> data = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), originator, latestTsKeys).get(); | 332 | List<TsKvEntry> data = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), originator, latestTsKeys).get(); |
313 | for (TsKvEntry entry : data) { | 333 | for (TsKvEntry entry : data) { |
314 | if (entry.getValue() != null) { | 334 | if (entry.getValue() != null) { |
315 | - result.putValue(new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()), toEntityValue(entry)); | 335 | + result.putValue(new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()), entry.getTs(), toEntityValue(entry)); |
316 | } | 336 | } |
317 | } | 337 | } |
318 | } | 338 | } |
@@ -330,13 +350,13 @@ class DeviceState { | @@ -330,13 +350,13 @@ class DeviceState { | ||
330 | } | 350 | } |
331 | } | 351 | } |
332 | 352 | ||
333 | - private void addToSnapshot(DeviceDataSnapshot snapshot, Set<String> commonAttributeKeys, List<AttributeKvEntry> data) { | 353 | + private void addToSnapshot(DataSnapshot snapshot, Set<String> commonAttributeKeys, List<AttributeKvEntry> data) { |
334 | for (AttributeKvEntry entry : data) { | 354 | for (AttributeKvEntry entry : data) { |
335 | if (entry.getValue() != null) { | 355 | if (entry.getValue() != null) { |
336 | EntityKeyValue value = toEntityValue(entry); | 356 | EntityKeyValue value = toEntityValue(entry); |
337 | - snapshot.putValue(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, entry.getKey()), value); | 357 | + snapshot.putValue(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, entry.getKey()), entry.getLastUpdateTs(), value); |
338 | if (commonAttributeKeys.contains(entry.getKey())) { | 358 | if (commonAttributeKeys.contains(entry.getKey())) { |
339 | - snapshot.putValue(new EntityKey(EntityKeyType.ATTRIBUTE, entry.getKey()), value); | 359 | + snapshot.putValue(new EntityKey(EntityKeyType.ATTRIBUTE, entry.getKey()), entry.getLastUpdateTs(), value); |
340 | } | 360 | } |
341 | } | 361 | } |
342 | } | 362 | } |
@@ -15,9 +15,11 @@ | @@ -15,9 +15,11 @@ | ||
15 | */ | 15 | */ |
16 | package org.thingsboard.rule.engine.profile; | 16 | package org.thingsboard.rule.engine.profile; |
17 | 17 | ||
18 | +import lombok.EqualsAndHashCode; | ||
18 | import lombok.Getter; | 19 | import lombok.Getter; |
19 | import org.thingsboard.server.common.data.kv.DataType; | 20 | import org.thingsboard.server.common.data.kv.DataType; |
20 | 21 | ||
22 | +@EqualsAndHashCode | ||
21 | class EntityKeyValue { | 23 | class EntityKeyValue { |
22 | 24 | ||
23 | @Getter | 25 | @Getter |
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java
renamed from
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileState.java
@@ -18,19 +18,33 @@ package org.thingsboard.rule.engine.profile; | @@ -18,19 +18,33 @@ package org.thingsboard.rule.engine.profile; | ||
18 | import lombok.AccessLevel; | 18 | import lombok.AccessLevel; |
19 | import lombok.Getter; | 19 | import lombok.Getter; |
20 | import org.thingsboard.server.common.data.DeviceProfile; | 20 | import org.thingsboard.server.common.data.DeviceProfile; |
21 | +import org.thingsboard.server.common.data.alarm.AlarmSeverity; | ||
21 | import org.thingsboard.server.common.data.device.profile.AlarmRule; | 22 | import org.thingsboard.server.common.data.device.profile.AlarmRule; |
22 | import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; | 23 | import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; |
23 | import org.thingsboard.server.common.data.id.DeviceProfileId; | 24 | import org.thingsboard.server.common.data.id.DeviceProfileId; |
25 | +import org.thingsboard.server.common.data.query.ComplexFilterPredicate; | ||
26 | +import org.thingsboard.server.common.data.query.DynamicValue; | ||
27 | +import org.thingsboard.server.common.data.query.DynamicValueSourceType; | ||
24 | import org.thingsboard.server.common.data.query.EntityKey; | 28 | import org.thingsboard.server.common.data.query.EntityKey; |
29 | +import org.thingsboard.server.common.data.query.EntityKeyType; | ||
30 | +import org.thingsboard.server.common.data.query.FilterPredicateValue; | ||
25 | import org.thingsboard.server.common.data.query.KeyFilter; | 31 | import org.thingsboard.server.common.data.query.KeyFilter; |
32 | +import org.thingsboard.server.common.data.query.KeyFilterPredicate; | ||
33 | +import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate; | ||
34 | +import org.thingsboard.server.common.data.query.StringFilterPredicate; | ||
26 | 35 | ||
36 | +import javax.print.attribute.standard.Severity; | ||
37 | +import java.util.Collections; | ||
38 | +import java.util.HashMap; | ||
39 | +import java.util.HashSet; | ||
27 | import java.util.List; | 40 | import java.util.List; |
41 | +import java.util.Map; | ||
28 | import java.util.Set; | 42 | import java.util.Set; |
29 | import java.util.concurrent.ConcurrentHashMap; | 43 | import java.util.concurrent.ConcurrentHashMap; |
30 | import java.util.concurrent.CopyOnWriteArrayList; | 44 | import java.util.concurrent.CopyOnWriteArrayList; |
31 | 45 | ||
32 | 46 | ||
33 | -class DeviceProfileState { | 47 | +class ProfileState { |
34 | 48 | ||
35 | private DeviceProfile deviceProfile; | 49 | private DeviceProfile deviceProfile; |
36 | @Getter(AccessLevel.PACKAGE) | 50 | @Getter(AccessLevel.PACKAGE) |
@@ -38,26 +52,86 @@ class DeviceProfileState { | @@ -38,26 +52,86 @@ class DeviceProfileState { | ||
38 | @Getter(AccessLevel.PACKAGE) | 52 | @Getter(AccessLevel.PACKAGE) |
39 | private final Set<EntityKey> entityKeys = ConcurrentHashMap.newKeySet(); | 53 | private final Set<EntityKey> entityKeys = ConcurrentHashMap.newKeySet(); |
40 | 54 | ||
41 | - DeviceProfileState(DeviceProfile deviceProfile) { | 55 | + private final Map<String, Map<AlarmSeverity, Set<EntityKey>>> alarmCreateKeys = new HashMap<>(); |
56 | + private final Map<String, Set<EntityKey>> alarmClearKeys = new HashMap<>(); | ||
57 | + | ||
58 | + ProfileState(DeviceProfile deviceProfile) { | ||
42 | updateDeviceProfile(deviceProfile); | 59 | updateDeviceProfile(deviceProfile); |
43 | } | 60 | } |
44 | 61 | ||
45 | void updateDeviceProfile(DeviceProfile deviceProfile) { | 62 | void updateDeviceProfile(DeviceProfile deviceProfile) { |
46 | this.deviceProfile = deviceProfile; | 63 | this.deviceProfile = deviceProfile; |
47 | alarmSettings.clear(); | 64 | alarmSettings.clear(); |
65 | + alarmCreateKeys.clear(); | ||
66 | + alarmClearKeys.clear(); | ||
48 | if (deviceProfile.getProfileData().getAlarms() != null) { | 67 | if (deviceProfile.getProfileData().getAlarms() != null) { |
49 | alarmSettings.addAll(deviceProfile.getProfileData().getAlarms()); | 68 | alarmSettings.addAll(deviceProfile.getProfileData().getAlarms()); |
50 | for (DeviceProfileAlarm alarm : deviceProfile.getProfileData().getAlarms()) { | 69 | for (DeviceProfileAlarm alarm : deviceProfile.getProfileData().getAlarms()) { |
51 | - for (AlarmRule alarmRule : alarm.getCreateRules().values()) { | 70 | + Map<AlarmSeverity, Set<EntityKey>> createAlarmKeys = alarmCreateKeys.computeIfAbsent(alarm.getId(), id -> new HashMap<>()); |
71 | + alarm.getCreateRules().forEach(((severity, alarmRule) -> { | ||
72 | + Set<EntityKey> ruleKeys = createAlarmKeys.computeIfAbsent(severity, id -> new HashSet<>()); | ||
52 | for (KeyFilter keyFilter : alarmRule.getCondition().getCondition()) { | 73 | for (KeyFilter keyFilter : alarmRule.getCondition().getCondition()) { |
53 | entityKeys.add(keyFilter.getKey()); | 74 | entityKeys.add(keyFilter.getKey()); |
75 | + ruleKeys.add(keyFilter.getKey()); | ||
76 | + addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, ruleKeys); | ||
77 | + } | ||
78 | + })); | ||
79 | + if (alarm.getClearRule() != null) { | ||
80 | + Set<EntityKey> clearAlarmKeys = alarmClearKeys.computeIfAbsent(alarm.getId(), id -> new HashSet<>()); | ||
81 | + for (KeyFilter keyFilter : alarm.getClearRule().getCondition().getCondition()) { | ||
82 | + entityKeys.add(keyFilter.getKey()); | ||
83 | + clearAlarmKeys.add(keyFilter.getKey()); | ||
84 | + addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, clearAlarmKeys); | ||
54 | } | 85 | } |
55 | } | 86 | } |
56 | } | 87 | } |
57 | } | 88 | } |
58 | } | 89 | } |
59 | 90 | ||
60 | - public DeviceProfileId getProfileId() { | 91 | + private void addDynamicValuesRecursively(KeyFilterPredicate predicate, Set<EntityKey> entityKeys, Set<EntityKey> ruleKeys) { |
92 | + switch (predicate.getType()) { | ||
93 | + case STRING: | ||
94 | + case NUMERIC: | ||
95 | + case BOOLEAN: | ||
96 | + DynamicValue value = ((SimpleKeyFilterPredicate) predicate).getValue().getDynamicValue(); | ||
97 | + if (value != null && value.getSourceType() == DynamicValueSourceType.CURRENT_DEVICE) { | ||
98 | + EntityKey entityKey = new EntityKey(EntityKeyType.ATTRIBUTE, value.getSourceAttribute()); | ||
99 | + entityKeys.add(entityKey); | ||
100 | + ruleKeys.add(entityKey); | ||
101 | + } | ||
102 | + break; | ||
103 | + case COMPLEX: | ||
104 | + for (KeyFilterPredicate child : ((ComplexFilterPredicate) predicate).getPredicates()) { | ||
105 | + addDynamicValuesRecursively(child, entityKeys, ruleKeys); | ||
106 | + } | ||
107 | + break; | ||
108 | + } | ||
109 | + } | ||
110 | + | ||
111 | + DeviceProfileId getProfileId() { | ||
61 | return deviceProfile.getId(); | 112 | return deviceProfile.getId(); |
62 | } | 113 | } |
114 | + | ||
115 | + Set<EntityKey> getCreateAlarmKeys(String id, AlarmSeverity severity) { | ||
116 | + Map<AlarmSeverity, Set<EntityKey>> sKeys = alarmCreateKeys.get(id); | ||
117 | + if (sKeys == null) { | ||
118 | + return Collections.emptySet(); | ||
119 | + } else { | ||
120 | + Set<EntityKey> keys = sKeys.get(severity); | ||
121 | + if (keys == null) { | ||
122 | + return Collections.emptySet(); | ||
123 | + } else { | ||
124 | + return keys; | ||
125 | + } | ||
126 | + } | ||
127 | + } | ||
128 | + | ||
129 | + Set<EntityKey> getClearAlarmKeys(String id) { | ||
130 | + Set<EntityKey> keys = alarmClearKeys.get(id); | ||
131 | + if (keys == null) { | ||
132 | + return Collections.emptySet(); | ||
133 | + } else { | ||
134 | + return keys; | ||
135 | + } | ||
136 | + } | ||
63 | } | 137 | } |
1 | +/** | ||
2 | + * Copyright © 2016-2020 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 | +package org.thingsboard.rule.engine.profile; | ||
17 | + | ||
18 | +import lombok.Getter; | ||
19 | +import org.thingsboard.server.common.data.query.EntityKey; | ||
20 | +import org.thingsboard.server.common.data.query.EntityKeyType; | ||
21 | + | ||
22 | +import java.util.Set; | ||
23 | + | ||
24 | +class SnapshotUpdate { | ||
25 | + | ||
26 | + @Getter | ||
27 | + private final EntityKeyType type; | ||
28 | + @Getter | ||
29 | + private final Set<EntityKey> keys; | ||
30 | + | ||
31 | + SnapshotUpdate(EntityKeyType type, Set<EntityKey> keys) { | ||
32 | + this.type = type; | ||
33 | + this.keys = keys; | ||
34 | + } | ||
35 | + | ||
36 | + boolean hasUpdate(){ | ||
37 | + return !keys.isEmpty(); | ||
38 | + } | ||
39 | +} |
@@ -23,7 +23,6 @@ import org.thingsboard.rule.engine.api.TbNode; | @@ -23,7 +23,6 @@ import org.thingsboard.rule.engine.api.TbNode; | ||
23 | import org.thingsboard.rule.engine.api.TbNodeConfiguration; | 23 | import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
24 | import org.thingsboard.rule.engine.api.TbNodeException; | 24 | import org.thingsboard.rule.engine.api.TbNodeException; |
25 | import org.thingsboard.rule.engine.api.util.TbNodeUtils; | 25 | import org.thingsboard.rule.engine.api.util.TbNodeUtils; |
26 | -import org.thingsboard.rule.engine.profile.state.PersistedDeviceState; | ||
27 | import org.thingsboard.server.common.data.DataConstants; | 26 | import org.thingsboard.server.common.data.DataConstants; |
28 | import org.thingsboard.server.common.data.Device; | 27 | import org.thingsboard.server.common.data.Device; |
29 | import org.thingsboard.server.common.data.DeviceProfile; | 28 | import org.thingsboard.server.common.data.DeviceProfile; |
@@ -36,11 +35,10 @@ import org.thingsboard.server.common.data.plugin.ComponentType; | @@ -36,11 +35,10 @@ import org.thingsboard.server.common.data.plugin.ComponentType; | ||
36 | import org.thingsboard.server.common.data.rule.RuleNodeState; | 35 | import org.thingsboard.server.common.data.rule.RuleNodeState; |
37 | import org.thingsboard.server.common.msg.TbMsg; | 36 | import org.thingsboard.server.common.msg.TbMsg; |
38 | import org.thingsboard.server.common.msg.TbMsgMetaData; | 37 | import org.thingsboard.server.common.msg.TbMsgMetaData; |
38 | +import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; | ||
39 | import org.thingsboard.server.dao.util.mapping.JacksonUtil; | 39 | import org.thingsboard.server.dao.util.mapping.JacksonUtil; |
40 | 40 | ||
41 | -import java.util.HashMap; | ||
42 | import java.util.Map; | 41 | import java.util.Map; |
43 | -import java.util.UUID; | ||
44 | import java.util.concurrent.ConcurrentHashMap; | 42 | import java.util.concurrent.ConcurrentHashMap; |
45 | import java.util.concurrent.ExecutionException; | 43 | import java.util.concurrent.ExecutionException; |
46 | import java.util.concurrent.TimeUnit; | 44 | import java.util.concurrent.TimeUnit; |
@@ -70,11 +68,14 @@ public class TbDeviceProfileNode implements TbNode { | @@ -70,11 +68,14 @@ public class TbDeviceProfileNode implements TbNode { | ||
70 | this.cache = ctx.getDeviceProfileCache(); | 68 | this.cache = ctx.getDeviceProfileCache(); |
71 | scheduleAlarmHarvesting(ctx); | 69 | scheduleAlarmHarvesting(ctx); |
72 | if (config.isFetchAlarmRulesStateOnStart()) { | 70 | if (config.isFetchAlarmRulesStateOnStart()) { |
71 | + log.info("[{}] Fetching alarm rule state", ctx.getSelfId()); | ||
72 | + int fetchCount = 0; | ||
73 | PageLink pageLink = new PageLink(1024); | 73 | PageLink pageLink = new PageLink(1024); |
74 | while (true) { | 74 | while (true) { |
75 | PageData<RuleNodeState> states = ctx.findRuleNodeStates(pageLink); | 75 | PageData<RuleNodeState> states = ctx.findRuleNodeStates(pageLink); |
76 | if (!states.getData().isEmpty()) { | 76 | if (!states.getData().isEmpty()) { |
77 | for (RuleNodeState rns : states.getData()) { | 77 | for (RuleNodeState rns : states.getData()) { |
78 | + fetchCount++; | ||
78 | if (rns.getEntityId().getEntityType().equals(EntityType.DEVICE) && ctx.isLocalEntity(rns.getEntityId())) { | 79 | if (rns.getEntityId().getEntityType().equals(EntityType.DEVICE) && ctx.isLocalEntity(rns.getEntityId())) { |
79 | getOrCreateDeviceState(ctx, new DeviceId(rns.getEntityId().getId()), rns); | 80 | getOrCreateDeviceState(ctx, new DeviceId(rns.getEntityId().getId()), rns); |
80 | } | 81 | } |
@@ -86,6 +87,11 @@ public class TbDeviceProfileNode implements TbNode { | @@ -86,6 +87,11 @@ public class TbDeviceProfileNode implements TbNode { | ||
86 | pageLink = pageLink.nextPageLink(); | 87 | pageLink = pageLink.nextPageLink(); |
87 | } | 88 | } |
88 | } | 89 | } |
90 | + log.info("[{}] Fetched alarm rule state for {} entities", ctx.getSelfId(), fetchCount); | ||
91 | + } | ||
92 | + if (!config.isPersistAlarmRulesState() && ctx.isLocalEntity(ctx.getSelfId())) { | ||
93 | + log.info("[{}] Going to cleanup rule node states", ctx.getSelfId()); | ||
94 | + ctx.clearRuleNodeStates(); | ||
89 | } | 95 | } |
90 | } | 96 | } |
91 | 97 | ||
@@ -114,11 +120,14 @@ public class TbDeviceProfileNode implements TbNode { | @@ -114,11 +120,14 @@ public class TbDeviceProfileNode implements TbNode { | ||
114 | } | 120 | } |
115 | } | 121 | } |
116 | } else if (EntityType.DEVICE_PROFILE.equals(originatorType)) { | 122 | } else if (EntityType.DEVICE_PROFILE.equals(originatorType)) { |
123 | + log.info("[{}] Received device profile update notification: {}", ctx.getSelfId(), msg.getData()); | ||
117 | if (msg.getType().equals("ENTITY_UPDATED")) { | 124 | if (msg.getType().equals("ENTITY_UPDATED")) { |
118 | DeviceProfile deviceProfile = JacksonUtil.fromString(msg.getData(), DeviceProfile.class); | 125 | DeviceProfile deviceProfile = JacksonUtil.fromString(msg.getData(), DeviceProfile.class); |
119 | - for (DeviceState state : deviceStates.values()) { | ||
120 | - if (deviceProfile.getId().equals(state.getProfileId())) { | ||
121 | - state.updateProfile(ctx, deviceProfile); | 126 | + if (deviceProfile != null) { |
127 | + for (DeviceState state : deviceStates.values()) { | ||
128 | + if (deviceProfile.getId().equals(state.getProfileId())) { | ||
129 | + state.updateProfile(ctx, deviceProfile); | ||
130 | + } | ||
122 | } | 131 | } |
123 | } | 132 | } |
124 | } | 133 | } |
@@ -141,6 +150,12 @@ public class TbDeviceProfileNode implements TbNode { | @@ -141,6 +150,12 @@ public class TbDeviceProfileNode implements TbNode { | ||
141 | } | 150 | } |
142 | 151 | ||
143 | @Override | 152 | @Override |
153 | + public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) { | ||
154 | + // Cleanup the cache for all entities that are no longer assigned to current server partitions | ||
155 | + deviceStates.entrySet().removeIf(entry -> !ctx.isLocalEntity(entry.getKey())); | ||
156 | + } | ||
157 | + | ||
158 | + @Override | ||
144 | public void destroy() { | 159 | public void destroy() { |
145 | deviceStates.clear(); | 160 | deviceStates.clear(); |
146 | } | 161 | } |
@@ -150,7 +165,7 @@ public class TbDeviceProfileNode implements TbNode { | @@ -150,7 +165,7 @@ public class TbDeviceProfileNode implements TbNode { | ||
150 | if (deviceState == null) { | 165 | if (deviceState == null) { |
151 | DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId); | 166 | DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId); |
152 | if (deviceProfile != null) { | 167 | if (deviceProfile != null) { |
153 | - deviceState = new DeviceState(ctx, config, deviceId, new DeviceProfileState(deviceProfile), rns); | 168 | + deviceState = new DeviceState(ctx, config, deviceId, new ProfileState(deviceProfile), rns); |
154 | deviceStates.put(deviceId, deviceState); | 169 | deviceStates.put(deviceId, deviceState); |
155 | } | 170 | } |
156 | } | 171 | } |
@@ -102,13 +102,16 @@ import { DeviceProfileAlarmComponent } from './profile/alarm/device-profile-alar | @@ -102,13 +102,16 @@ import { DeviceProfileAlarmComponent } from './profile/alarm/device-profile-alar | ||
102 | import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component'; | 102 | import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component'; |
103 | import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component'; | 103 | import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component'; |
104 | import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component'; | 104 | import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component'; |
105 | -import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component'; | ||
106 | import { FilterTextComponent } from './filter/filter-text.component'; | 105 | import { FilterTextComponent } from './filter/filter-text.component'; |
107 | import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; | 106 | import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; |
108 | import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component'; | 107 | import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component'; |
109 | import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component'; | 108 | import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component'; |
110 | import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component'; | 109 | import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component'; |
111 | import { DeviceCredentialsComponent } from './device/device-credentials.component'; | 110 | import { DeviceCredentialsComponent } from './device/device-credentials.component'; |
111 | +import { AlarmScheduleInfoComponent } from './profile/alarm/alarm-schedule-info.component'; | ||
112 | +import { AlarmScheduleDialogComponent } from '@home/components/profile/alarm/alarm-schedule-dialog.component'; | ||
113 | +import { EditAlarmDetailsDialogComponent } from './profile/alarm/edit-alarm-details-dialog.component'; | ||
114 | +import { AlarmRuleConditionDialogComponent } from '@home/components/profile/alarm/alarm-rule-condition-dialog.component'; | ||
112 | 115 | ||
113 | @NgModule({ | 116 | @NgModule({ |
114 | declarations: | 117 | declarations: |
@@ -190,7 +193,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen | @@ -190,7 +193,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen | ||
190 | DeviceProfileTransportConfigurationComponent, | 193 | DeviceProfileTransportConfigurationComponent, |
191 | CreateAlarmRulesComponent, | 194 | CreateAlarmRulesComponent, |
192 | AlarmRuleComponent, | 195 | AlarmRuleComponent, |
193 | - AlarmRuleKeyFiltersDialogComponent, | 196 | + AlarmRuleConditionDialogComponent, |
194 | AlarmRuleConditionComponent, | 197 | AlarmRuleConditionComponent, |
195 | DeviceProfileAlarmComponent, | 198 | DeviceProfileAlarmComponent, |
196 | DeviceProfileAlarmsComponent, | 199 | DeviceProfileAlarmsComponent, |
@@ -198,9 +201,12 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen | @@ -198,9 +201,12 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen | ||
198 | DeviceProfileDialogComponent, | 201 | DeviceProfileDialogComponent, |
199 | AddDeviceProfileDialogComponent, | 202 | AddDeviceProfileDialogComponent, |
200 | RuleChainAutocompleteComponent, | 203 | RuleChainAutocompleteComponent, |
204 | + AlarmScheduleInfoComponent, | ||
201 | AlarmScheduleComponent, | 205 | AlarmScheduleComponent, |
202 | DeviceWizardDialogComponent, | 206 | DeviceWizardDialogComponent, |
203 | - DeviceCredentialsComponent | 207 | + DeviceCredentialsComponent, |
208 | + AlarmScheduleDialogComponent, | ||
209 | + EditAlarmDetailsDialogComponent | ||
204 | ], | 210 | ], |
205 | imports: [ | 211 | imports: [ |
206 | CommonModule, | 212 | CommonModule, |
@@ -271,7 +277,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen | @@ -271,7 +277,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen | ||
271 | DeviceProfileTransportConfigurationComponent, | 277 | DeviceProfileTransportConfigurationComponent, |
272 | CreateAlarmRulesComponent, | 278 | CreateAlarmRulesComponent, |
273 | AlarmRuleComponent, | 279 | AlarmRuleComponent, |
274 | - AlarmRuleKeyFiltersDialogComponent, | 280 | + AlarmRuleConditionDialogComponent, |
275 | AlarmRuleConditionComponent, | 281 | AlarmRuleConditionComponent, |
276 | DeviceProfileAlarmComponent, | 282 | DeviceProfileAlarmComponent, |
277 | DeviceProfileAlarmsComponent, | 283 | DeviceProfileAlarmsComponent, |
@@ -281,7 +287,10 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen | @@ -281,7 +287,10 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen | ||
281 | RuleChainAutocompleteComponent, | 287 | RuleChainAutocompleteComponent, |
282 | DeviceWizardDialogComponent, | 288 | DeviceWizardDialogComponent, |
283 | DeviceCredentialsComponent, | 289 | DeviceCredentialsComponent, |
284 | - AlarmScheduleComponent | 290 | + AlarmScheduleInfoComponent, |
291 | + AlarmScheduleComponent, | ||
292 | + AlarmScheduleDialogComponent, | ||
293 | + EditAlarmDetailsDialogComponent | ||
285 | ], | 294 | ], |
286 | providers: [ | 295 | providers: [ |
287 | WidgetComponentService, | 296 | WidgetComponentService, |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html
0 → 100644
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2020 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 | +<form [formGroup]="conditionFormGroup" (ngSubmit)="save()" style="width: 700px;"> | ||
19 | + <mat-toolbar color="primary"> | ||
20 | + <h2>{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}</h2> | ||
21 | + <span fxFlex></span> | ||
22 | + <button mat-icon-button | ||
23 | + (click)="cancel()" | ||
24 | + type="button"> | ||
25 | + <mat-icon class="material-icons">close</mat-icon> | ||
26 | + </button> | ||
27 | + </mat-toolbar> | ||
28 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
29 | + </mat-progress-bar> | ||
30 | + <div mat-dialog-content> | ||
31 | + <fieldset [disabled]="isLoading$ | async"> | ||
32 | + <div fxFlex fxLayout="column"> | ||
33 | + <tb-key-filter-list | ||
34 | + [displayUserParameters]="false" | ||
35 | + [allowUserDynamicSource]="false" | ||
36 | + [telemetryKeysOnly]="true" | ||
37 | + formControlName="keyFilters"> | ||
38 | + </tb-key-filter-list> | ||
39 | + <section formGroupName="spec" class="row"> | ||
40 | + <mat-form-field class="mat-block" hideRequiredMarker> | ||
41 | + <mat-label translate>device-profile.condition-type</mat-label> | ||
42 | + <mat-select formControlName="type" required> | ||
43 | + <mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType"> | ||
44 | + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} | ||
45 | + </mat-option> | ||
46 | + </mat-select> | ||
47 | + <mat-error *ngIf="conditionFormGroup.get('spec.type').hasError('required')"> | ||
48 | + {{ 'device-profile.condition-type-required' | translate }} | ||
49 | + </mat-error> | ||
50 | + </mat-form-field> | ||
51 | + <div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.DURATION"> | ||
52 | + <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always"> | ||
53 | + <mat-label></mat-label> | ||
54 | + <input type="number" required | ||
55 | + step="1" min="1" max="2147483647" matInput | ||
56 | + placeholder="{{ 'device-profile.condition-duration-value' | translate }}" | ||
57 | + formControlName="value"> | ||
58 | + <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('required')"> | ||
59 | + {{ 'device-profile.condition-duration-value-required' | translate }} | ||
60 | + </mat-error> | ||
61 | + <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('min')"> | ||
62 | + {{ 'device-profile.condition-duration-value-range' | translate }} | ||
63 | + </mat-error> | ||
64 | + <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('max')"> | ||
65 | + {{ 'device-profile.condition-duration-value-range' | translate }} | ||
66 | + </mat-error> | ||
67 | + <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('pattern')"> | ||
68 | + {{ 'device-profile.condition-duration-value-pattern' | translate }} | ||
69 | + </mat-error> | ||
70 | + </mat-form-field> | ||
71 | + <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always"> | ||
72 | + <mat-label></mat-label> | ||
73 | + <mat-select formControlName="unit" | ||
74 | + required | ||
75 | + placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}"> | ||
76 | + <mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit"> | ||
77 | + {{ timeUnitTranslations.get(timeUnit) | translate }} | ||
78 | + </mat-option> | ||
79 | + </mat-select> | ||
80 | + <mat-error *ngIf="conditionFormGroup.get('spec.unit').hasError('required')"> | ||
81 | + {{ 'device-profile.condition-duration-time-unit-required' | translate }} | ||
82 | + </mat-error> | ||
83 | + </mat-form-field> | ||
84 | + </div> | ||
85 | + <div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.REPEATING"> | ||
86 | + <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always"> | ||
87 | + <mat-label></mat-label> | ||
88 | + <input type="number" required | ||
89 | + step="1" min="1" max="2147483647" matInput | ||
90 | + placeholder="{{ 'device-profile.condition-repeating-value' | translate }}" | ||
91 | + formControlName="count"> | ||
92 | + <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('required')"> | ||
93 | + {{ 'device-profile.condition-repeating-value-required' | translate }} | ||
94 | + </mat-error> | ||
95 | + <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('min')"> | ||
96 | + {{ 'device-profile.condition-repeating-value-range' | translate }} | ||
97 | + </mat-error> | ||
98 | + <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('max')"> | ||
99 | + {{ 'device-profile.condition-repeating-value-range' | translate }} | ||
100 | + </mat-error> | ||
101 | + <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('pattern')"> | ||
102 | + {{ 'device-profile.condition-repeating-value-pattern' | translate }} | ||
103 | + </mat-error> | ||
104 | + </mat-form-field> | ||
105 | + </div> | ||
106 | + </section> | ||
107 | + </div> | ||
108 | + </fieldset> | ||
109 | + </div> | ||
110 | + <div mat-dialog-actions fxLayoutAlign="end center"> | ||
111 | + <button mat-raised-button color="primary" | ||
112 | + *ngIf="!readonly" | ||
113 | + type="submit" | ||
114 | + [disabled]="(isLoading$ | async) || conditionFormGroup.invalid || !conditionFormGroup.dirty"> | ||
115 | + {{ 'action.save' | translate }} | ||
116 | + </button> | ||
117 | + <button mat-button color="primary" | ||
118 | + type="button" | ||
119 | + [disabled]="(isLoading$ | async)" | ||
120 | + (click)="cancel()" cdkFocusInitial> | ||
121 | + {{ (readonly ? 'action.close' : 'action.cancel') | translate }} | ||
122 | + </button> | ||
123 | + </div> | ||
124 | +</form> |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss
renamed from
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyState.java
@@ -13,10 +13,8 @@ | @@ -13,10 +13,8 @@ | ||
13 | * See the License for the specific language governing permissions and | 13 | * See the License for the specific language governing permissions and |
14 | * limitations under the License. | 14 | * limitations under the License. |
15 | */ | 15 | */ |
16 | -package org.thingsboard.rule.engine.profile; | ||
17 | - | ||
18 | -public class EntityKeyState { | ||
19 | - | ||
20 | - | ||
21 | - | 16 | +:host { |
17 | + .row { | ||
18 | + margin-top: 1em; | ||
19 | + } | ||
22 | } | 20 | } |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts
0 → 100644
1 | +/// | ||
2 | +/// Copyright © 2016-2020 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, Inject, OnInit, SkipSelf } from '@angular/core'; | ||
18 | +import { ErrorStateMatcher } from '@angular/material/core'; | ||
19 | +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; | ||
20 | +import { Store } from '@ngrx/store'; | ||
21 | +import { AppState } from '@core/core.state'; | ||
22 | +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; | ||
23 | +import { Router } from '@angular/router'; | ||
24 | +import { DialogComponent } from '@app/shared/components/dialog.component'; | ||
25 | +import { UtilsService } from '@core/services/utils.service'; | ||
26 | +import { TranslateService } from '@ngx-translate/core'; | ||
27 | +import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models'; | ||
28 | +import { AlarmCondition, AlarmConditionType, AlarmConditionTypeTranslationMap } from '@shared/models/device.models'; | ||
29 | +import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; | ||
30 | + | ||
31 | +export interface AlarmRuleConditionDialogData { | ||
32 | + readonly: boolean; | ||
33 | + condition: AlarmCondition; | ||
34 | +} | ||
35 | + | ||
36 | +@Component({ | ||
37 | + selector: 'tb-alarm-rule-condition-dialog', | ||
38 | + templateUrl: './alarm-rule-condition-dialog.component.html', | ||
39 | + providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleConditionDialogComponent}], | ||
40 | + styleUrls: ['/alarm-rule-condition-dialog.component.scss'] | ||
41 | +}) | ||
42 | +export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRuleConditionDialogComponent, AlarmCondition> | ||
43 | + implements OnInit, ErrorStateMatcher { | ||
44 | + | ||
45 | + timeUnits = Object.keys(TimeUnit); | ||
46 | + timeUnitTranslations = timeUnitTranslationMap; | ||
47 | + alarmConditionTypes = Object.keys(AlarmConditionType); | ||
48 | + AlarmConditionType = AlarmConditionType; | ||
49 | + alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap; | ||
50 | + | ||
51 | + readonly = this.data.readonly; | ||
52 | + condition = this.data.condition; | ||
53 | + | ||
54 | + conditionFormGroup: FormGroup; | ||
55 | + | ||
56 | + submitted = false; | ||
57 | + | ||
58 | + constructor(protected store: Store<AppState>, | ||
59 | + protected router: Router, | ||
60 | + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleConditionDialogData, | ||
61 | + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | ||
62 | + public dialogRef: MatDialogRef<AlarmRuleConditionDialogComponent, AlarmCondition>, | ||
63 | + private fb: FormBuilder, | ||
64 | + private utils: UtilsService, | ||
65 | + public translate: TranslateService) { | ||
66 | + super(store, router, dialogRef); | ||
67 | + | ||
68 | + this.conditionFormGroup = this.fb.group({ | ||
69 | + keyFilters: [keyFiltersToKeyFilterInfos(this.condition?.condition), Validators.required], | ||
70 | + spec: this.fb.group({ | ||
71 | + type: [AlarmConditionType.SIMPLE, Validators.required], | ||
72 | + unit: [{value: null, disable: true}, Validators.required], | ||
73 | + value: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]], | ||
74 | + count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]] | ||
75 | + }) | ||
76 | + }); | ||
77 | + this.conditionFormGroup.patchValue({spec: this.condition?.spec}); | ||
78 | + this.conditionFormGroup.get('spec.type').valueChanges.subscribe((type) => { | ||
79 | + this.updateValidators(type, true, true); | ||
80 | + }); | ||
81 | + if (this.readonly) { | ||
82 | + this.conditionFormGroup.disable({emitEvent: false}); | ||
83 | + } else { | ||
84 | + this.updateValidators(this.condition?.spec?.type); | ||
85 | + } | ||
86 | + } | ||
87 | + | ||
88 | + ngOnInit(): void { | ||
89 | + } | ||
90 | + | ||
91 | + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | ||
92 | + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); | ||
93 | + const customErrorState = !!(control && control.invalid && this.submitted); | ||
94 | + return originalErrorState || customErrorState; | ||
95 | + } | ||
96 | + | ||
97 | + private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) { | ||
98 | + switch (type) { | ||
99 | + case AlarmConditionType.DURATION: | ||
100 | + this.conditionFormGroup.get('spec.value').enable(); | ||
101 | + this.conditionFormGroup.get('spec.unit').enable(); | ||
102 | + this.conditionFormGroup.get('spec.count').disable(); | ||
103 | + if (resetDuration) { | ||
104 | + this.conditionFormGroup.get('spec').patchValue({ | ||
105 | + count: null | ||
106 | + }); | ||
107 | + } | ||
108 | + break; | ||
109 | + case AlarmConditionType.REPEATING: | ||
110 | + this.conditionFormGroup.get('spec.count').enable(); | ||
111 | + this.conditionFormGroup.get('spec.value').disable(); | ||
112 | + this.conditionFormGroup.get('spec.unit').disable(); | ||
113 | + if (resetDuration) { | ||
114 | + this.conditionFormGroup.get('spec').patchValue({ | ||
115 | + value: null, | ||
116 | + unit: null | ||
117 | + }); | ||
118 | + } | ||
119 | + break; | ||
120 | + case AlarmConditionType.SIMPLE: | ||
121 | + this.conditionFormGroup.get('spec.value').disable(); | ||
122 | + this.conditionFormGroup.get('spec.unit').disable(); | ||
123 | + this.conditionFormGroup.get('spec.count').disable(); | ||
124 | + if (resetDuration) { | ||
125 | + this.conditionFormGroup.get('spec').patchValue({ | ||
126 | + value: null, | ||
127 | + unit: null, | ||
128 | + count: null | ||
129 | + }); | ||
130 | + } | ||
131 | + break; | ||
132 | + } | ||
133 | + this.conditionFormGroup.get('spec.value').updateValueAndValidity({emitEvent}); | ||
134 | + this.conditionFormGroup.get('spec.unit').updateValueAndValidity({emitEvent}); | ||
135 | + this.conditionFormGroup.get('spec.count').updateValueAndValidity({emitEvent}); | ||
136 | + } | ||
137 | + | ||
138 | + cancel(): void { | ||
139 | + this.dialogRef.close(null); | ||
140 | + } | ||
141 | + | ||
142 | + save(): void { | ||
143 | + this.submitted = true; | ||
144 | + this.condition = { | ||
145 | + condition: keyFilterInfosToKeyFilters(this.conditionFormGroup.get('keyFilters').value), | ||
146 | + spec: this.conditionFormGroup.get('spec').value | ||
147 | + }; | ||
148 | + this.dialogRef.close(this.condition); | ||
149 | + } | ||
150 | +} |
@@ -15,8 +15,9 @@ | @@ -15,8 +15,9 @@ | ||
15 | limitations under the License. | 15 | limitations under the License. |
16 | 16 | ||
17 | --> | 17 | --> |
18 | -<div fxLayout="column" fxFlex> | 18 | +<div fxLayout="column" fxFlex [formGroup]="alarmRuleConditionFormGroup"> |
19 | <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;"> | 19 | <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;"> |
20 | + <label class="tb-title" translate>device-profile.condition</label> | ||
20 | <span fxFlex></span> | 21 | <span fxFlex></span> |
21 | <a mat-button color="primary" | 22 | <a mat-button color="primary" |
22 | type="button" | 23 | type="button" |
@@ -27,9 +28,12 @@ | @@ -27,9 +28,12 @@ | ||
27 | </a> | 28 | </a> |
28 | </div> | 29 | </div> |
29 | <div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)"> | 30 | <div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)"> |
30 | - <tb-filter-text [formControl]="alarmRuleConditionControl" | 31 | + <tb-filter-text formControlName="condition" |
31 | required | 32 | required |
32 | addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}"> | 33 | addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}"> |
33 | </tb-filter-text> | 34 | </tb-filter-text> |
35 | + <span class="tb-alarm-rule-condition-spec" [ngClass]="{disabled: this.disabled}" [innerHTML]="specText"> | ||
36 | + </span> | ||
34 | </div> | 37 | </div> |
38 | + | ||
35 | </div> | 39 | </div> |
@@ -21,10 +21,15 @@ | @@ -21,10 +21,15 @@ | ||
21 | } | 21 | } |
22 | } | 22 | } |
23 | .tb-alarm-rule-condition { | 23 | .tb-alarm-rule-condition { |
24 | - padding: 8px; | ||
25 | - border: 1px groove rgba(0, 0, 0, .25); | ||
26 | - border-radius: 4px; | ||
27 | cursor: pointer; | 24 | cursor: pointer; |
25 | + .tb-alarm-rule-condition-spec { | ||
26 | + margin-top: 1em; | ||
27 | + line-height: 1.8em; | ||
28 | + padding: 4px; | ||
29 | + &.disabled { | ||
30 | + opacity: 0.7; | ||
31 | + } | ||
32 | + } | ||
28 | } | 33 | } |
29 | } | 34 | } |
30 | 35 |
@@ -18,20 +18,21 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core'; | @@ -18,20 +18,21 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core'; | ||
18 | import { | 18 | import { |
19 | ControlValueAccessor, | 19 | ControlValueAccessor, |
20 | FormBuilder, | 20 | FormBuilder, |
21 | - FormControl, | 21 | + FormControl, FormGroup, |
22 | NG_VALIDATORS, | 22 | NG_VALIDATORS, |
23 | NG_VALUE_ACCESSOR, | 23 | NG_VALUE_ACCESSOR, |
24 | - Validator | 24 | + Validator, Validators |
25 | } from '@angular/forms'; | 25 | } from '@angular/forms'; |
26 | import { MatDialog } from '@angular/material/dialog'; | 26 | import { MatDialog } from '@angular/material/dialog'; |
27 | -import { KeyFilter } from '@shared/models/query/query.models'; | ||
28 | -import { deepClone } from '@core/utils'; | ||
29 | -import { | ||
30 | - AlarmRuleKeyFiltersDialogComponent, | ||
31 | - AlarmRuleKeyFiltersDialogData | ||
32 | -} from './alarm-rule-key-filters-dialog.component'; | 27 | +import { deepClone, isUndefined } from '@core/utils'; |
33 | import { TranslateService } from '@ngx-translate/core'; | 28 | import { TranslateService } from '@ngx-translate/core'; |
34 | import { DatePipe } from '@angular/common'; | 29 | import { DatePipe } from '@angular/common'; |
30 | +import { AlarmCondition, AlarmConditionSpec, AlarmConditionType } from '@shared/models/device.models'; | ||
31 | +import { | ||
32 | + AlarmRuleConditionDialogComponent, | ||
33 | + AlarmRuleConditionDialogData | ||
34 | +} from '@home/components/profile/alarm/alarm-rule-condition-dialog.component'; | ||
35 | +import { TimeUnit } from '@shared/models/time/time.models'; | ||
35 | 36 | ||
36 | @Component({ | 37 | @Component({ |
37 | selector: 'tb-alarm-rule-condition', | 38 | selector: 'tb-alarm-rule-condition', |
@@ -55,9 +56,11 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit | @@ -55,9 +56,11 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit | ||
55 | @Input() | 56 | @Input() |
56 | disabled: boolean; | 57 | disabled: boolean; |
57 | 58 | ||
58 | - alarmRuleConditionControl: FormControl; | 59 | + alarmRuleConditionFormGroup: FormGroup; |
60 | + | ||
61 | + specText = ''; | ||
59 | 62 | ||
60 | - private modelValue: Array<KeyFilter>; | 63 | + private modelValue: AlarmCondition; |
61 | 64 | ||
62 | private propagateChange = (v: any) => { }; | 65 | private propagateChange = (v: any) => { }; |
63 | 66 | ||
@@ -75,25 +78,31 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit | @@ -75,25 +78,31 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit | ||
75 | } | 78 | } |
76 | 79 | ||
77 | ngOnInit() { | 80 | ngOnInit() { |
78 | - this.alarmRuleConditionControl = this.fb.control(null); | 81 | + this.alarmRuleConditionFormGroup = this.fb.group({ |
82 | + condition: [null, Validators.required], | ||
83 | + spec: [null, Validators.required] | ||
84 | + }); | ||
79 | } | 85 | } |
80 | 86 | ||
81 | setDisabledState(isDisabled: boolean): void { | 87 | setDisabledState(isDisabled: boolean): void { |
82 | this.disabled = isDisabled; | 88 | this.disabled = isDisabled; |
83 | if (this.disabled) { | 89 | if (this.disabled) { |
84 | - this.alarmRuleConditionControl.disable({emitEvent: false}); | 90 | + this.alarmRuleConditionFormGroup.disable({emitEvent: false}); |
85 | } else { | 91 | } else { |
86 | - this.alarmRuleConditionControl.enable({emitEvent: false}); | 92 | + this.alarmRuleConditionFormGroup.enable({emitEvent: false}); |
87 | } | 93 | } |
88 | } | 94 | } |
89 | 95 | ||
90 | - writeValue(value: Array<KeyFilter>): void { | 96 | + writeValue(value: AlarmCondition): void { |
91 | this.modelValue = value; | 97 | this.modelValue = value; |
98 | + if (this.modelValue !== null && isUndefined(this.modelValue?.spec)) { | ||
99 | + this.modelValue = Object.assign(this.modelValue, {spec: {type: AlarmConditionType.SIMPLE}}); | ||
100 | + } | ||
92 | this.updateConditionInfo(); | 101 | this.updateConditionInfo(); |
93 | } | 102 | } |
94 | 103 | ||
95 | public conditionSet() { | 104 | public conditionSet() { |
96 | - return this.modelValue && this.modelValue.length; | 105 | + return this.modelValue && this.modelValue.condition.length; |
97 | } | 106 | } |
98 | 107 | ||
99 | public validate(c: FormControl) { | 108 | public validate(c: FormControl) { |
@@ -108,13 +117,13 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit | @@ -108,13 +117,13 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit | ||
108 | if ($event) { | 117 | if ($event) { |
109 | $event.stopPropagation(); | 118 | $event.stopPropagation(); |
110 | } | 119 | } |
111 | - this.dialog.open<AlarmRuleKeyFiltersDialogComponent, AlarmRuleKeyFiltersDialogData, | ||
112 | - Array<KeyFilter>>(AlarmRuleKeyFiltersDialogComponent, { | 120 | + this.dialog.open<AlarmRuleConditionDialogComponent, AlarmRuleConditionDialogData, |
121 | + AlarmCondition>(AlarmRuleConditionDialogComponent, { | ||
113 | disableClose: true, | 122 | disableClose: true, |
114 | panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | 123 | panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
115 | data: { | 124 | data: { |
116 | readonly: this.disabled, | 125 | readonly: this.disabled, |
117 | - keyFilters: this.disabled ? this.modelValue : deepClone(this.modelValue) | 126 | + condition: this.disabled ? this.modelValue : deepClone(this.modelValue) |
118 | } | 127 | } |
119 | }).afterClosed().subscribe((result) => { | 128 | }).afterClosed().subscribe((result) => { |
120 | if (result) { | 129 | if (result) { |
@@ -125,7 +134,45 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit | @@ -125,7 +134,45 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit | ||
125 | } | 134 | } |
126 | 135 | ||
127 | private updateConditionInfo() { | 136 | private updateConditionInfo() { |
128 | - this.alarmRuleConditionControl.patchValue(this.modelValue); | 137 | + this.alarmRuleConditionFormGroup.patchValue( |
138 | + { | ||
139 | + condition: this.modelValue?.condition, | ||
140 | + spec: this.modelValue?.spec | ||
141 | + } | ||
142 | + ); | ||
143 | + this.updateSpecText(); | ||
144 | + } | ||
145 | + | ||
146 | + private updateSpecText() { | ||
147 | + this.specText = ''; | ||
148 | + if (this.modelValue && this.modelValue.spec) { | ||
149 | + const spec = this.modelValue.spec; | ||
150 | + switch (spec.type) { | ||
151 | + case AlarmConditionType.SIMPLE: | ||
152 | + break; | ||
153 | + case AlarmConditionType.DURATION: | ||
154 | + let duringText = ''; | ||
155 | + switch (spec.unit) { | ||
156 | + case TimeUnit.SECONDS: | ||
157 | + duringText = this.translate.instant('timewindow.seconds', {seconds: spec.value}); | ||
158 | + break; | ||
159 | + case TimeUnit.MINUTES: | ||
160 | + duringText = this.translate.instant('timewindow.minutes', {minutes: spec.value}); | ||
161 | + break; | ||
162 | + case TimeUnit.HOURS: | ||
163 | + duringText = this.translate.instant('timewindow.hours', {hours: spec.value}); | ||
164 | + break; | ||
165 | + case TimeUnit.DAYS: | ||
166 | + duringText = this.translate.instant('timewindow.days', {days: spec.value}); | ||
167 | + break; | ||
168 | + } | ||
169 | + this.specText = this.translate.instant('device-profile.condition-during', {during: duringText}); | ||
170 | + break; | ||
171 | + case AlarmConditionType.REPEATING: | ||
172 | + this.specText = this.translate.instant('device-profile.condition-repeat-times', {count: spec.count}); | ||
173 | + break; | ||
174 | + } | ||
175 | + } | ||
129 | } | 176 | } |
130 | 177 | ||
131 | private updateModel() { | 178 | private updateModel() { |
@@ -16,90 +16,36 @@ | @@ -16,90 +16,36 @@ | ||
16 | 16 | ||
17 | --> | 17 | --> |
18 | <div fxLayout="column" [formGroup]="alarmRuleFormGroup"> | 18 | <div fxLayout="column" [formGroup]="alarmRuleFormGroup"> |
19 | - <mat-tab-group> | ||
20 | - <mat-tab label="{{ 'device-profile.condition' | translate }}" formGroupName="condition"> | ||
21 | - <tb-alarm-rule-condition fxFlex class="row" | ||
22 | - formControlName="condition"> | ||
23 | - </tb-alarm-rule-condition> | ||
24 | - <section formGroupName="spec" class="row"> | ||
25 | - <mat-form-field class="mat-block" hideRequiredMarker> | ||
26 | - <mat-label translate>device-profile.condition-type</mat-label> | ||
27 | - <mat-select formControlName="type" required> | ||
28 | - <mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType"> | ||
29 | - {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} | ||
30 | - </mat-option> | ||
31 | - </mat-select> | ||
32 | - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.type').hasError('required')"> | ||
33 | - {{ 'device-profile.condition-type-required' | translate }} | ||
34 | - </mat-error> | ||
35 | - </mat-form-field> | ||
36 | - <div fxLayout="row" fxLayoutGap="8px" *ngIf="alarmRuleFormGroup.get('condition.spec.type').value == AlarmConditionType.DURATION"> | ||
37 | - <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always"> | ||
38 | - <mat-label></mat-label> | ||
39 | - <input type="number" required | ||
40 | - step="1" min="1" max="2147483647" matInput | ||
41 | - placeholder="{{ 'device-profile.condition-duration-value' | translate }}" | ||
42 | - formControlName="value"> | ||
43 | - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('required')"> | ||
44 | - {{ 'device-profile.condition-duration-value-required' | translate }} | ||
45 | - </mat-error> | ||
46 | - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('min')"> | ||
47 | - {{ 'device-profile.condition-duration-value-range' | translate }} | ||
48 | - </mat-error> | ||
49 | - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('max')"> | ||
50 | - {{ 'device-profile.condition-duration-value-range' | translate }} | ||
51 | - </mat-error> | ||
52 | - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('pattern')"> | ||
53 | - {{ 'device-profile.condition-duration-value-pattern' | translate }} | ||
54 | - </mat-error> | ||
55 | - </mat-form-field> | ||
56 | - <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always"> | ||
57 | - <mat-label></mat-label> | ||
58 | - <mat-select formControlName="unit" | ||
59 | - required | ||
60 | - placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}"> | ||
61 | - <mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit"> | ||
62 | - {{ timeUnitTranslations.get(timeUnit) | translate }} | ||
63 | - </mat-option> | ||
64 | - </mat-select> | ||
65 | - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.unit').hasError('required')"> | ||
66 | - {{ 'device-profile.condition-duration-time-unit-required' | translate }} | ||
67 | - </mat-error> | ||
68 | - </mat-form-field> | ||
69 | - </div> | ||
70 | - <div fxLayout="row" fxLayoutGap="8px" *ngIf="alarmRuleFormGroup.get('condition.spec.type').value == AlarmConditionType.REPEATING"> | ||
71 | - <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always"> | ||
72 | - <mat-label></mat-label> | ||
73 | - <input type="number" required | ||
74 | - step="1" min="1" max="2147483647" matInput | ||
75 | - placeholder="{{ 'device-profile.condition-repeating-value' | translate }}" | ||
76 | - formControlName="count"> | ||
77 | - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('required')"> | ||
78 | - {{ 'device-profile.condition-repeating-value-required' | translate }} | ||
79 | - </mat-error> | ||
80 | - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('min')"> | ||
81 | - {{ 'device-profile.condition-repeating-value-range' | translate }} | ||
82 | - </mat-error> | ||
83 | - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('max')"> | ||
84 | - {{ 'device-profile.condition-repeating-value-range' | translate }} | ||
85 | - </mat-error> | ||
86 | - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('pattern')"> | ||
87 | - {{ 'device-profile.condition-repeating-value-pattern' | translate }} | ||
88 | - </mat-error> | ||
89 | - </mat-form-field> | ||
90 | - </div> | ||
91 | - </section> | ||
92 | - </mat-tab> | ||
93 | - <mat-tab label="{{ 'device-profile.schedule' | translate }}"> | ||
94 | - <tb-alarm-schedule fxFlex class="row" | ||
95 | - formControlName="schedule"> | ||
96 | - </tb-alarm-schedule> | ||
97 | - </mat-tab> | ||
98 | - <mat-tab label="{{ 'device-profile.alarm-rule-details' | translate }}"> | ||
99 | - <mat-form-field class="mat-block row"> | ||
100 | - <mat-label translate>device-profile.alarm-details</mat-label> | ||
101 | - <textarea matInput formControlName="alarmDetails" rows="5"></textarea> | ||
102 | - </mat-form-field> | ||
103 | - </mat-tab> | ||
104 | - </mat-tab-group> | 19 | + <tb-alarm-rule-condition fxFlex class="row" |
20 | + formControlName="condition"> | ||
21 | + </tb-alarm-rule-condition> | ||
22 | + <mat-divider class="row"></mat-divider> | ||
23 | + <tb-alarm-schedule-info fxFlex class="row" | ||
24 | + formControlName="schedule"> | ||
25 | + </tb-alarm-schedule-info> | ||
26 | + <mat-divider class="row"></mat-divider> | ||
27 | + <div fxLayout="column" fxFlex class="tb-alarm-rule-details row"> | ||
28 | + <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;"> | ||
29 | + <label class="tb-title" translate>device-profile.alarm-rule-details</label> | ||
30 | + <span fxFlex></span> | ||
31 | + <a mat-button color="primary" | ||
32 | + *ngIf="!disabled" | ||
33 | + type="button" | ||
34 | + (click)="openEditDetailsDialog($event)" | ||
35 | + matTooltip="{{ 'action.edit' | translate }}" | ||
36 | + matTooltipPosition="above"> | ||
37 | + {{ 'action.edit' | translate }} | ||
38 | + </a> | ||
39 | + </div> | ||
40 | + <div fxLayout="row" fxLayoutAlign="start start"> | ||
41 | + <div class="tb-alarm-rule-details-content" [ngClass]="{disabled: this.disabled, collapsed: !this.expandAlarmDetails}" | ||
42 | + (click)="!disabled ? openEditDetailsDialog($event) : {}" | ||
43 | + fxFlex [innerHTML]="alarmRuleFormGroup.get('alarmDetails').value"></div> | ||
44 | + <a mat-button color="primary" | ||
45 | + type="button" | ||
46 | + (click)="expandAlarmDetails = !expandAlarmDetails"> | ||
47 | + {{ (expandAlarmDetails ? 'action.hide' : 'action.read-more') | translate }} | ||
48 | + </a> | ||
49 | + </div> | ||
50 | + </div> | ||
105 | </div> | 51 | </div> |
@@ -17,5 +17,29 @@ | @@ -17,5 +17,29 @@ | ||
17 | .row { | 17 | .row { |
18 | margin-top: 1em; | 18 | margin-top: 1em; |
19 | } | 19 | } |
20 | + .tb-alarm-rule-details { | ||
21 | + a.mat-button { | ||
22 | + &:hover, &:focus { | ||
23 | + border-bottom: none; | ||
24 | + } | ||
25 | + } | ||
26 | + .tb-alarm-rule-details-content { | ||
27 | + min-height: 33px; | ||
28 | + overflow: hidden; | ||
29 | + white-space: pre; | ||
30 | + line-height: 1.8em; | ||
31 | + padding: 4px; | ||
32 | + cursor: pointer; | ||
33 | + &.collapsed { | ||
34 | + max-height: 33px; | ||
35 | + white-space: nowrap; | ||
36 | + text-overflow: ellipsis; | ||
37 | + } | ||
38 | + &.disabled { | ||
39 | + opacity: 0.7; | ||
40 | + cursor: auto; | ||
41 | + } | ||
42 | + } | ||
43 | + } | ||
20 | } | 44 | } |
21 | 45 |
@@ -25,11 +25,14 @@ import { | @@ -25,11 +25,14 @@ import { | ||
25 | Validator, | 25 | Validator, |
26 | Validators | 26 | Validators |
27 | } from '@angular/forms'; | 27 | } from '@angular/forms'; |
28 | -import { AlarmConditionType, AlarmConditionTypeTranslationMap, AlarmRule } from '@shared/models/device.models'; | 28 | +import { AlarmRule } from '@shared/models/device.models'; |
29 | import { MatDialog } from '@angular/material/dialog'; | 29 | import { MatDialog } from '@angular/material/dialog'; |
30 | -import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; | ||
31 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; | 30 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
32 | -import { isUndefined } from '@core/utils'; | 31 | +import { isDefinedAndNotNull } from '@core/utils'; |
32 | +import { | ||
33 | + EditAlarmDetailsDialogComponent, | ||
34 | + EditAlarmDetailsDialogData | ||
35 | +} from '@home/components/profile/alarm/edit-alarm-details-dialog.component'; | ||
33 | 36 | ||
34 | @Component({ | 37 | @Component({ |
35 | selector: 'tb-alarm-rule', | 38 | selector: 'tb-alarm-rule', |
@@ -50,12 +53,6 @@ import { isUndefined } from '@core/utils'; | @@ -50,12 +53,6 @@ import { isUndefined } from '@core/utils'; | ||
50 | }) | 53 | }) |
51 | export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { | 54 | export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { |
52 | 55 | ||
53 | - timeUnits = Object.keys(TimeUnit); | ||
54 | - timeUnitTranslations = timeUnitTranslationMap; | ||
55 | - alarmConditionTypes = Object.keys(AlarmConditionType); | ||
56 | - AlarmConditionType = AlarmConditionType; | ||
57 | - alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap; | ||
58 | - | ||
59 | @Input() | 56 | @Input() |
60 | disabled: boolean; | 57 | disabled: boolean; |
61 | 58 | ||
@@ -72,6 +69,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat | @@ -72,6 +69,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat | ||
72 | 69 | ||
73 | alarmRuleFormGroup: FormGroup; | 70 | alarmRuleFormGroup: FormGroup; |
74 | 71 | ||
72 | + expandAlarmDetails = false; | ||
73 | + | ||
75 | private propagateChange = (v: any) => { }; | 74 | private propagateChange = (v: any) => { }; |
76 | 75 | ||
77 | constructor(private dialog: MatDialog, | 76 | constructor(private dialog: MatDialog, |
@@ -87,21 +86,10 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat | @@ -87,21 +86,10 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat | ||
87 | 86 | ||
88 | ngOnInit() { | 87 | ngOnInit() { |
89 | this.alarmRuleFormGroup = this.fb.group({ | 88 | this.alarmRuleFormGroup = this.fb.group({ |
90 | - condition: this.fb.group({ | ||
91 | - condition: [null, Validators.required], | ||
92 | - spec: this.fb.group({ | ||
93 | - type: [AlarmConditionType.SIMPLE, Validators.required], | ||
94 | - unit: [{value: null, disable: true}, Validators.required], | ||
95 | - value: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]], | ||
96 | - count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]] | ||
97 | - }) | ||
98 | - }, Validators.required), | 89 | + condition: [null, [Validators.required]], |
99 | schedule: [null], | 90 | schedule: [null], |
100 | alarmDetails: [null] | 91 | alarmDetails: [null] |
101 | }); | 92 | }); |
102 | - this.alarmRuleFormGroup.get('condition.spec.type').valueChanges.subscribe((type) => { | ||
103 | - this.updateValidators(type, true, true); | ||
104 | - }); | ||
105 | this.alarmRuleFormGroup.valueChanges.subscribe(() => { | 93 | this.alarmRuleFormGroup.valueChanges.subscribe(() => { |
106 | this.updateModel(); | 94 | this.updateModel(); |
107 | }); | 95 | }); |
@@ -118,11 +106,25 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat | @@ -118,11 +106,25 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat | ||
118 | 106 | ||
119 | writeValue(value: AlarmRule): void { | 107 | writeValue(value: AlarmRule): void { |
120 | this.modelValue = value; | 108 | this.modelValue = value; |
121 | - if (this.modelValue !== null && isUndefined(this.modelValue?.condition?.spec)) { | ||
122 | - this.modelValue = Object.assign(this.modelValue, {condition: {spec: {type: AlarmConditionType.SIMPLE}}}); | ||
123 | - } | ||
124 | this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); | 109 | this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); |
125 | - this.updateValidators(this.modelValue?.condition?.spec?.type); | 110 | + } |
111 | + | ||
112 | + public openEditDetailsDialog($event: Event) { | ||
113 | + if ($event) { | ||
114 | + $event.stopPropagation(); | ||
115 | + } | ||
116 | + this.dialog.open<EditAlarmDetailsDialogComponent, EditAlarmDetailsDialogData, | ||
117 | + string>(EditAlarmDetailsDialogComponent, { | ||
118 | + disableClose: true, | ||
119 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | ||
120 | + data: { | ||
121 | + alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value | ||
122 | + } | ||
123 | + }).afterClosed().subscribe((alarmDetails) => { | ||
124 | + if (isDefinedAndNotNull(alarmDetails)) { | ||
125 | + this.alarmRuleFormGroup.patchValue({alarmDetails}); | ||
126 | + } | ||
127 | + }); | ||
126 | } | 128 | } |
127 | 129 | ||
128 | public validate(c: FormControl) { | 130 | public validate(c: FormControl) { |
@@ -133,47 +135,6 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat | @@ -133,47 +135,6 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat | ||
133 | }; | 135 | }; |
134 | } | 136 | } |
135 | 137 | ||
136 | - private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) { | ||
137 | - switch (type) { | ||
138 | - case AlarmConditionType.DURATION: | ||
139 | - this.alarmRuleFormGroup.get('condition.spec.value').enable(); | ||
140 | - this.alarmRuleFormGroup.get('condition.spec.unit').enable(); | ||
141 | - this.alarmRuleFormGroup.get('condition.spec.count').disable(); | ||
142 | - if (resetDuration) { | ||
143 | - this.alarmRuleFormGroup.get('condition.spec').patchValue({ | ||
144 | - count: null | ||
145 | - }); | ||
146 | - } | ||
147 | - break; | ||
148 | - case AlarmConditionType.REPEATING: | ||
149 | - this.alarmRuleFormGroup.get('condition.spec.count').enable(); | ||
150 | - this.alarmRuleFormGroup.get('condition.spec.value').disable(); | ||
151 | - this.alarmRuleFormGroup.get('condition.spec.unit').disable(); | ||
152 | - if (resetDuration) { | ||
153 | - this.alarmRuleFormGroup.get('condition.spec').patchValue({ | ||
154 | - value: null, | ||
155 | - unit: null | ||
156 | - }); | ||
157 | - } | ||
158 | - break; | ||
159 | - case AlarmConditionType.SIMPLE: | ||
160 | - this.alarmRuleFormGroup.get('condition.spec.value').disable(); | ||
161 | - this.alarmRuleFormGroup.get('condition.spec.unit').disable(); | ||
162 | - this.alarmRuleFormGroup.get('condition.spec.count').disable(); | ||
163 | - if (resetDuration) { | ||
164 | - this.alarmRuleFormGroup.get('condition.spec').patchValue({ | ||
165 | - value: null, | ||
166 | - unit: null, | ||
167 | - count: null | ||
168 | - }); | ||
169 | - } | ||
170 | - break; | ||
171 | - } | ||
172 | - this.alarmRuleFormGroup.get('condition.spec.value').updateValueAndValidity({emitEvent}); | ||
173 | - this.alarmRuleFormGroup.get('condition.spec.unit').updateValueAndValidity({emitEvent}); | ||
174 | - this.alarmRuleFormGroup.get('condition.spec.count').updateValueAndValidity({emitEvent}); | ||
175 | - } | ||
176 | - | ||
177 | private updateModel() { | 138 | private updateModel() { |
178 | const value = this.alarmRuleFormGroup.value; | 139 | const value = this.alarmRuleFormGroup.value; |
179 | if (this.modelValue) { | 140 | if (this.modelValue) { |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html
renamed from
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html
@@ -15,9 +15,9 @@ | @@ -15,9 +15,9 @@ | ||
15 | limitations under the License. | 15 | limitations under the License. |
16 | 16 | ||
17 | --> | 17 | --> |
18 | -<form [formGroup]="keyFiltersFormGroup" (ngSubmit)="save()" style="width: 700px;"> | 18 | +<form [formGroup]="alarmScheduleFormGroup" (ngSubmit)="save()" style="width: 800px;"> |
19 | <mat-toolbar color="primary"> | 19 | <mat-toolbar color="primary"> |
20 | - <h2>{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}</h2> | 20 | + <h2>{{ (readonly ? 'device-profile.schedule' : 'device-profile.edit-schedule') | translate }}</h2> |
21 | <span fxFlex></span> | 21 | <span fxFlex></span> |
22 | <button mat-icon-button | 22 | <button mat-icon-button |
23 | (click)="cancel()" | 23 | (click)="cancel()" |
@@ -30,12 +30,9 @@ | @@ -30,12 +30,9 @@ | ||
30 | <div mat-dialog-content> | 30 | <div mat-dialog-content> |
31 | <fieldset [disabled]="isLoading$ | async"> | 31 | <fieldset [disabled]="isLoading$ | async"> |
32 | <div fxFlex fxLayout="column"> | 32 | <div fxFlex fxLayout="column"> |
33 | - <tb-key-filter-list | ||
34 | - [displayUserParameters]="false" | ||
35 | - [allowUserDynamicSource]="false" | ||
36 | - [telemetryKeysOnly]="true" | ||
37 | - formControlName="keyFilters"> | ||
38 | - </tb-key-filter-list> | 33 | + <tb-alarm-schedule |
34 | + formControlName="alarmSchedule"> | ||
35 | + </tb-alarm-schedule> | ||
39 | </div> | 36 | </div> |
40 | </fieldset> | 37 | </fieldset> |
41 | </div> | 38 | </div> |
@@ -43,7 +40,7 @@ | @@ -43,7 +40,7 @@ | ||
43 | <button mat-raised-button color="primary" | 40 | <button mat-raised-button color="primary" |
44 | *ngIf="!readonly" | 41 | *ngIf="!readonly" |
45 | type="submit" | 42 | type="submit" |
46 | - [disabled]="(isLoading$ | async) || keyFiltersFormGroup.invalid || !keyFiltersFormGroup.dirty"> | 43 | + [disabled]="(isLoading$ | async) || alarmScheduleFormGroup.invalid || !alarmScheduleFormGroup.dirty"> |
47 | {{ 'action.save' | translate }} | 44 | {{ 'action.save' | translate }} |
48 | </button> | 45 | </button> |
49 | <button mat-button color="primary" | 46 | <button mat-button color="primary" |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.ts
renamed from
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.ts
@@ -19,49 +19,49 @@ import { ErrorStateMatcher } from '@angular/material/core'; | @@ -19,49 +19,49 @@ import { ErrorStateMatcher } from '@angular/material/core'; | ||
19 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; | 19 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
20 | import { Store } from '@ngrx/store'; | 20 | import { Store } from '@ngrx/store'; |
21 | import { AppState } from '@core/core.state'; | 21 | import { AppState } from '@core/core.state'; |
22 | -import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; | 22 | +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; |
23 | import { Router } from '@angular/router'; | 23 | import { Router } from '@angular/router'; |
24 | import { DialogComponent } from '@app/shared/components/dialog.component'; | 24 | import { DialogComponent } from '@app/shared/components/dialog.component'; |
25 | import { UtilsService } from '@core/services/utils.service'; | 25 | import { UtilsService } from '@core/services/utils.service'; |
26 | import { TranslateService } from '@ngx-translate/core'; | 26 | import { TranslateService } from '@ngx-translate/core'; |
27 | -import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models'; | 27 | +import { AlarmSchedule } from '@shared/models/device.models'; |
28 | 28 | ||
29 | -export interface AlarmRuleKeyFiltersDialogData { | 29 | +export interface AlarmScheduleDialogData { |
30 | readonly: boolean; | 30 | readonly: boolean; |
31 | - keyFilters: Array<KeyFilter>; | 31 | + alarmSchedule: AlarmSchedule; |
32 | } | 32 | } |
33 | 33 | ||
34 | @Component({ | 34 | @Component({ |
35 | - selector: 'tb-alarm-rule-key-filters-dialog', | ||
36 | - templateUrl: './alarm-rule-key-filters-dialog.component.html', | ||
37 | - providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleKeyFiltersDialogComponent}], | 35 | + selector: 'tb-alarm-schedule-dialog', |
36 | + templateUrl: './alarm-schedule-dialog.component.html', | ||
37 | + providers: [{provide: ErrorStateMatcher, useExisting: AlarmScheduleDialogComponent}], | ||
38 | styleUrls: [] | 38 | styleUrls: [] |
39 | }) | 39 | }) |
40 | -export class AlarmRuleKeyFiltersDialogComponent extends DialogComponent<AlarmRuleKeyFiltersDialogComponent, Array<KeyFilter>> | 40 | +export class AlarmScheduleDialogComponent extends DialogComponent<AlarmScheduleDialogComponent, AlarmSchedule> |
41 | implements OnInit, ErrorStateMatcher { | 41 | implements OnInit, ErrorStateMatcher { |
42 | 42 | ||
43 | readonly = this.data.readonly; | 43 | readonly = this.data.readonly; |
44 | - keyFilters = this.data.keyFilters; | 44 | + alarmSchedule = this.data.alarmSchedule; |
45 | 45 | ||
46 | - keyFiltersFormGroup: FormGroup; | 46 | + alarmScheduleFormGroup: FormGroup; |
47 | 47 | ||
48 | submitted = false; | 48 | submitted = false; |
49 | 49 | ||
50 | constructor(protected store: Store<AppState>, | 50 | constructor(protected store: Store<AppState>, |
51 | protected router: Router, | 51 | protected router: Router, |
52 | - @Inject(MAT_DIALOG_DATA) public data: AlarmRuleKeyFiltersDialogData, | 52 | + @Inject(MAT_DIALOG_DATA) public data: AlarmScheduleDialogData, |
53 | @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | 53 | @SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
54 | - public dialogRef: MatDialogRef<AlarmRuleKeyFiltersDialogComponent, Array<KeyFilter>>, | 54 | + public dialogRef: MatDialogRef<AlarmScheduleDialogComponent, AlarmSchedule>, |
55 | private fb: FormBuilder, | 55 | private fb: FormBuilder, |
56 | private utils: UtilsService, | 56 | private utils: UtilsService, |
57 | public translate: TranslateService) { | 57 | public translate: TranslateService) { |
58 | super(store, router, dialogRef); | 58 | super(store, router, dialogRef); |
59 | 59 | ||
60 | - this.keyFiltersFormGroup = this.fb.group({ | ||
61 | - keyFilters: [keyFiltersToKeyFilterInfos(this.keyFilters), Validators.required] | 60 | + this.alarmScheduleFormGroup = this.fb.group({ |
61 | + alarmSchedule: [this.alarmSchedule] | ||
62 | }); | 62 | }); |
63 | if (this.readonly) { | 63 | if (this.readonly) { |
64 | - this.keyFiltersFormGroup.disable({emitEvent: false}); | 64 | + this.alarmScheduleFormGroup.disable({emitEvent: false}); |
65 | } | 65 | } |
66 | } | 66 | } |
67 | 67 | ||
@@ -80,7 +80,7 @@ export class AlarmRuleKeyFiltersDialogComponent extends DialogComponent<AlarmRul | @@ -80,7 +80,7 @@ export class AlarmRuleKeyFiltersDialogComponent extends DialogComponent<AlarmRul | ||
80 | 80 | ||
81 | save(): void { | 81 | save(): void { |
82 | this.submitted = true; | 82 | this.submitted = true; |
83 | - this.keyFilters = keyFilterInfosToKeyFilters(this.keyFiltersFormGroup.get('keyFilters').value); | ||
84 | - this.dialogRef.close(this.keyFilters); | 83 | + this.alarmSchedule = this.alarmScheduleFormGroup.get('alarmSchedule').value; |
84 | + this.dialogRef.close(this.alarmSchedule); | ||
85 | } | 85 | } |
86 | } | 86 | } |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2020 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 fxLayout="column" fxFlex> | ||
19 | + <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;"> | ||
20 | + <label class="tb-title" translate>device-profile.schedule</label> | ||
21 | + <span fxFlex></span> | ||
22 | + <a mat-button color="primary" | ||
23 | + type="button" | ||
24 | + (click)="openScheduleDialog($event)" | ||
25 | + matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}" | ||
26 | + matTooltipPosition="above"> | ||
27 | + {{ (disabled ? 'action.view' : 'action.edit' ) | translate }} | ||
28 | + </a> | ||
29 | + </div> | ||
30 | + <sapn class="tb-alarm-rule-schedule" [ngClass]="{disabled: this.disabled}" (click)="openScheduleDialog($event)" | ||
31 | + [innerHTML]="scheduleText"> | ||
32 | + </sapn> | ||
33 | +</div> |
1 | +/** | ||
2 | + * Copyright © 2016-2020 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 | +:host { | ||
17 | + display: flex; | ||
18 | + a.mat-button { | ||
19 | + &:hover, &:focus { | ||
20 | + border-bottom: none; | ||
21 | + } | ||
22 | + } | ||
23 | + .tb-alarm-rule-schedule { | ||
24 | + line-height: 1.8em; | ||
25 | + padding: 4px; | ||
26 | + cursor: pointer; | ||
27 | + &.disabled { | ||
28 | + opacity: 0.7; | ||
29 | + } | ||
30 | + .nowrap { | ||
31 | + white-space: nowrap; | ||
32 | + } | ||
33 | + } | ||
34 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2020 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 { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; | ||
18 | +import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; | ||
19 | +import { | ||
20 | + AlarmSchedule, | ||
21 | + AlarmScheduleType, | ||
22 | + AlarmScheduleTypeTranslationMap, dayOfWeekTranslations, | ||
23 | + getAlarmScheduleRangeText, utcTimestampToTimeOfDay | ||
24 | +} from '@shared/models/device.models'; | ||
25 | +import { MatDialog } from '@angular/material/dialog'; | ||
26 | +import { | ||
27 | + AlarmScheduleDialogComponent, | ||
28 | + AlarmScheduleDialogData | ||
29 | +} from '@home/components/profile/alarm/alarm-schedule-dialog.component'; | ||
30 | +import { deepClone, isDefinedAndNotNull } from '@core/utils'; | ||
31 | +import { TranslateService } from '@ngx-translate/core'; | ||
32 | + | ||
33 | +@Component({ | ||
34 | + selector: 'tb-alarm-schedule-info', | ||
35 | + templateUrl: './alarm-schedule-info.component.html', | ||
36 | + styleUrls: ['./alarm-schedule-info.component.scss'], | ||
37 | + providers: [{ | ||
38 | + provide: NG_VALUE_ACCESSOR, | ||
39 | + useExisting: forwardRef(() => AlarmScheduleInfoComponent), | ||
40 | + multi: true | ||
41 | + }] | ||
42 | +}) | ||
43 | +export class AlarmScheduleInfoComponent implements ControlValueAccessor, OnInit { | ||
44 | + | ||
45 | + @Input() | ||
46 | + disabled: boolean; | ||
47 | + | ||
48 | + private modelValue: AlarmSchedule; | ||
49 | + | ||
50 | + scheduleText = ''; | ||
51 | + | ||
52 | + private propagateChange = (v: any) => { }; | ||
53 | + | ||
54 | + constructor(private dialog: MatDialog, | ||
55 | + private translate: TranslateService, | ||
56 | + private cd: ChangeDetectorRef) { | ||
57 | + } | ||
58 | + | ||
59 | + ngOnInit(): void { | ||
60 | + } | ||
61 | + | ||
62 | + registerOnChange(fn: any): void { | ||
63 | + this.propagateChange = fn; | ||
64 | + } | ||
65 | + | ||
66 | + registerOnTouched(fn: any): void { | ||
67 | + } | ||
68 | + | ||
69 | + setDisabledState(isDisabled: boolean): void { | ||
70 | + this.disabled = isDisabled; | ||
71 | + } | ||
72 | + | ||
73 | + writeValue(value: AlarmSchedule): void { | ||
74 | + this.modelValue = value; | ||
75 | + this.updateScheduleText(); | ||
76 | + } | ||
77 | + | ||
78 | + private updateScheduleText() { | ||
79 | + let schedule = this.modelValue; | ||
80 | + if (!isDefinedAndNotNull(schedule)) { | ||
81 | + schedule = { | ||
82 | + type: AlarmScheduleType.ANY_TIME | ||
83 | + }; | ||
84 | + } | ||
85 | + this.scheduleText = ''; | ||
86 | + switch (schedule.type) { | ||
87 | + case AlarmScheduleType.ANY_TIME: | ||
88 | + this.scheduleText = this.translate.instant('device-profile.schedule-any-time'); | ||
89 | + break; | ||
90 | + case AlarmScheduleType.SPECIFIC_TIME: | ||
91 | + for (const day of schedule.daysOfWeek) { | ||
92 | + if (this.scheduleText.length) { | ||
93 | + this.scheduleText += ', '; | ||
94 | + } | ||
95 | + this.scheduleText += this.translate.instant(dayOfWeekTranslations[day - 1]); | ||
96 | + } | ||
97 | + this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(schedule.startsOn), | ||
98 | + utcTimestampToTimeOfDay(schedule.endsOn)) + '</b>'; | ||
99 | + break; | ||
100 | + case AlarmScheduleType.CUSTOM: | ||
101 | + for (const item of schedule.items) { | ||
102 | + if (item.enabled) { | ||
103 | + if (this.scheduleText.length) { | ||
104 | + this.scheduleText += '<br/>'; | ||
105 | + } | ||
106 | + this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]); | ||
107 | + this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn), | ||
108 | + utcTimestampToTimeOfDay(item.endsOn)) + '</b>'; | ||
109 | + } | ||
110 | + } | ||
111 | + break; | ||
112 | + } | ||
113 | + } | ||
114 | + | ||
115 | + public openScheduleDialog($event: Event) { | ||
116 | + if ($event) { | ||
117 | + $event.stopPropagation(); | ||
118 | + } | ||
119 | + this.dialog.open<AlarmScheduleDialogComponent, AlarmScheduleDialogData, | ||
120 | + AlarmSchedule>(AlarmScheduleDialogComponent, { | ||
121 | + disableClose: true, | ||
122 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | ||
123 | + data: { | ||
124 | + readonly: this.disabled, | ||
125 | + alarmSchedule: this.disabled ? this.modelValue : deepClone(this.modelValue) | ||
126 | + } | ||
127 | + }).afterClosed().subscribe((result) => { | ||
128 | + if (result) { | ||
129 | + this.modelValue = result; | ||
130 | + this.propagateChange(this.modelValue); | ||
131 | + this.updateScheduleText(); | ||
132 | + this.cd.detectChanges(); | ||
133 | + } | ||
134 | + }); | ||
135 | + } | ||
136 | + | ||
137 | +} |
@@ -35,33 +35,20 @@ | @@ -35,33 +35,20 @@ | ||
35 | </tb-timezone-select> | 35 | </tb-timezone-select> |
36 | <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME"> | 36 | <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME"> |
37 | <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div> | 37 | <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div> |
38 | - <div fxLayout="column" fxLayout.gt-md="row" fxLayoutGap="16px" style="padding-bottom: 16px;"> | 38 | + <div fxLayout="column" fxLayout.gt-md="row" fxLayoutGap="16px"> |
39 | <div fxLayout="row" fxLayoutGap="16px"> | 39 | <div fxLayout="row" fxLayoutGap="16px"> |
40 | - <mat-checkbox [formControl]="weeklyRepeatControl(0)"> | ||
41 | - {{ 'device-profile.schedule-day.monday' | translate }} | ||
42 | - </mat-checkbox> | ||
43 | - <mat-checkbox [formControl]="weeklyRepeatControl(1)"> | ||
44 | - {{ 'device-profile.schedule-day.tuesday' | translate }} | ||
45 | - </mat-checkbox> | ||
46 | - <mat-checkbox [formControl]="weeklyRepeatControl(2)"> | ||
47 | - {{ 'device-profile.schedule-day.wednesday' | translate }} | ||
48 | - </mat-checkbox> | ||
49 | - <mat-checkbox [formControl]="weeklyRepeatControl(3)"> | ||
50 | - {{ 'device-profile.schedule-day.thursday' | translate }} | 40 | + <mat-checkbox *ngFor="let day of firstRowDays" [formControl]="weeklyRepeatControl(day)"> |
41 | + {{ dayOfWeekTranslationsArray[day] | translate }} | ||
51 | </mat-checkbox> | 42 | </mat-checkbox> |
52 | </div> | 43 | </div> |
53 | <div fxLayout="row" fxLayoutGap="16px"> | 44 | <div fxLayout="row" fxLayoutGap="16px"> |
54 | - <mat-checkbox [formControl]="weeklyRepeatControl(4)"> | ||
55 | - {{ 'device-profile.schedule-day.friday' | translate }} | ||
56 | - </mat-checkbox> | ||
57 | - <mat-checkbox [formControl]="weeklyRepeatControl(5)"> | ||
58 | - {{ 'device-profile.schedule-day.saturday' | translate }} | ||
59 | - </mat-checkbox> | ||
60 | - <mat-checkbox [formControl]="weeklyRepeatControl(6)"> | ||
61 | - {{ 'device-profile.schedule-day.sunday' | translate }} | 45 | + <mat-checkbox *ngFor="let day of secondRowDays" [formControl]="weeklyRepeatControl(day)"> |
46 | + {{ dayOfWeekTranslationsArray[day] | translate }} | ||
62 | </mat-checkbox> | 47 | </mat-checkbox> |
63 | </div> | 48 | </div> |
64 | </div> | 49 | </div> |
50 | + <tb-error style="display: block;" [error]="alarmScheduleForm.get('daysOfWeek').hasError('dayOfWeeks') | ||
51 | + ? ('device-profile.schedule-days-of-week-required' | translate) : ''"></tb-error> | ||
65 | <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-time</div> | 52 | <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-time</div> |
66 | <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> | 53 | <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> |
67 | <div fxLayout="row" fxLayoutGap="8px" fxFlex.gt-md> | 54 | <div fxLayout="row" fxLayoutGap="8px" fxFlex.gt-md> |
@@ -87,169 +74,35 @@ | @@ -87,169 +74,35 @@ | ||
87 | </section> | 74 | </section> |
88 | <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM"> | 75 | <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM"> |
89 | <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div> | 76 | <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div> |
90 | - <div fxLayout="column" formArrayName="items" fxLayoutGap="1em"> | ||
91 | - <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="0" fxLayoutAlign="start center" fxLayoutAlign.xs="center start"> | ||
92 | - <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 0)"> | ||
93 | - {{ 'device-profile.schedule-day.monday' | translate }} | ||
94 | - </mat-checkbox> | ||
95 | - <div fxLayout="row" fxLayoutGap="8px" fxFlex> | ||
96 | - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> | ||
97 | - <mat-label translate>device-profile.schedule-time-from</mat-label> | ||
98 | - <mat-datetimepicker-toggle [for]="startTimePicker1" matPrefix></mat-datetimepicker-toggle> | ||
99 | - <mat-datetimepicker #startTimePicker1 type="time" openOnFocus="true"></mat-datetimepicker> | ||
100 | - <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker1"> | ||
101 | - </mat-form-field> | ||
102 | - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> | ||
103 | - <mat-label translate>device-profile.schedule-time-to</mat-label> | ||
104 | - <mat-datetimepicker-toggle [for]="endTimePicker1" matPrefix></mat-datetimepicker-toggle> | ||
105 | - <mat-datetimepicker #endTimePicker1 type="time" openOnFocus="true"></mat-datetimepicker> | ||
106 | - <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker1"> | ||
107 | - </mat-form-field> | ||
108 | - </div> | ||
109 | - <div fxFlex fxLayoutAlign="center center" | ||
110 | - style="text-align: center" | ||
111 | - [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(0))"> | ||
112 | - </div> | ||
113 | - </div> | ||
114 | - <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="1" fxLayoutAlign="start center" fxLayoutAlign.xs="center start"> | ||
115 | - <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 1)"> | ||
116 | - {{ 'device-profile.schedule-day.tuesday' | translate }} | ||
117 | - </mat-checkbox> | ||
118 | - <div fxLayout="row" fxLayoutGap="8px" fxFlex> | ||
119 | - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> | ||
120 | - <mat-label translate>device-profile.schedule-time-from</mat-label> | ||
121 | - <mat-datetimepicker-toggle [for]="startTimePicker2" matPrefix></mat-datetimepicker-toggle> | ||
122 | - <mat-datetimepicker #startTimePicker2 type="time" openOnFocus="true"></mat-datetimepicker> | ||
123 | - <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker2"> | ||
124 | - </mat-form-field> | ||
125 | - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> | ||
126 | - <mat-label translate>device-profile.schedule-time-to</mat-label> | ||
127 | - <mat-datetimepicker-toggle [for]="endTimePicker2" matPrefix></mat-datetimepicker-toggle> | ||
128 | - <mat-datetimepicker #endTimePicker2 type="time" openOnFocus="true"></mat-datetimepicker> | ||
129 | - <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker2"> | ||
130 | - </mat-form-field> | ||
131 | - </div> | ||
132 | - <div fxFlex fxLayoutAlign="center center" | ||
133 | - style="text-align: center" | ||
134 | - [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(1))"> | ||
135 | - </div> | ||
136 | - </div> | ||
137 | - <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="2" fxLayoutAlign="start center" fxLayoutAlign.xs="center start"> | ||
138 | - <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 2)"> | ||
139 | - {{ 'device-profile.schedule-day.wednesday' | translate }} | ||
140 | - </mat-checkbox> | ||
141 | - <div fxLayout="row" fxLayoutGap="8px" fxFlex> | ||
142 | - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> | ||
143 | - <mat-label translate>device-profile.schedule-time-from</mat-label> | ||
144 | - <mat-datetimepicker-toggle [for]="startTimePicker3" matPrefix></mat-datetimepicker-toggle> | ||
145 | - <mat-datetimepicker #startTimePicker3 type="time" openOnFocus="true"></mat-datetimepicker> | ||
146 | - <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker3"> | ||
147 | - </mat-form-field> | ||
148 | - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> | ||
149 | - <mat-label translate>device-profile.schedule-time-to</mat-label> | ||
150 | - <mat-datetimepicker-toggle [for]="endTimePicker3" matPrefix></mat-datetimepicker-toggle> | ||
151 | - <mat-datetimepicker #endTimePicker3 type="time" openOnFocus="true"></mat-datetimepicker> | ||
152 | - <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker3"> | ||
153 | - </mat-form-field> | ||
154 | - </div> | ||
155 | - <div fxFlex fxLayoutAlign="center center" | ||
156 | - style="text-align: center" | ||
157 | - [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(2))"> | ||
158 | - </div> | ||
159 | - </div> | ||
160 | - <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="3" fxLayoutAlign="start center" fxLayoutAlign.xs="center start"> | ||
161 | - <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 3)"> | ||
162 | - {{ 'device-profile.schedule-day.thursday' | translate }} | ||
163 | - </mat-checkbox> | ||
164 | - <div fxLayout="row" fxLayoutGap="8px" fxFlex> | ||
165 | - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> | ||
166 | - <mat-label translate>device-profile.schedule-time-from</mat-label> | ||
167 | - <mat-datetimepicker-toggle [for]="startTimePicker4" matPrefix></mat-datetimepicker-toggle> | ||
168 | - <mat-datetimepicker #startTimePicker4 type="time" openOnFocus="true"></mat-datetimepicker> | ||
169 | - <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker4"> | ||
170 | - </mat-form-field> | ||
171 | - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> | ||
172 | - <mat-label translate>device-profile.schedule-time-to</mat-label> | ||
173 | - <mat-datetimepicker-toggle [for]="endTimePicker4" matPrefix></mat-datetimepicker-toggle> | ||
174 | - <mat-datetimepicker #endTimePicker4 type="time" openOnFocus="true"></mat-datetimepicker> | ||
175 | - <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker4"> | ||
176 | - </mat-form-field> | ||
177 | - </div> | ||
178 | - <div fxFlex fxLayoutAlign="center center" | ||
179 | - style="text-align: center" | ||
180 | - [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(3))"> | ||
181 | - </div> | ||
182 | - </div> | ||
183 | - <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="4" fxLayoutAlign="start center" fxLayoutAlign.xs="center start"> | ||
184 | - <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 4)"> | ||
185 | - {{ 'device-profile.schedule-day.friday' | translate }} | ||
186 | - </mat-checkbox> | ||
187 | - <div fxLayout="row" fxLayoutGap="8px" fxFlex> | ||
188 | - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> | ||
189 | - <mat-label translate>device-profile.schedule-time-from</mat-label> | ||
190 | - <mat-datetimepicker-toggle [for]="startTimePicker5" matPrefix></mat-datetimepicker-toggle> | ||
191 | - <mat-datetimepicker #startTimePicker5 type="time" openOnFocus="true"></mat-datetimepicker> | ||
192 | - <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker5"> | ||
193 | - </mat-form-field> | ||
194 | - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> | ||
195 | - <mat-label translate>device-profile.schedule-time-to</mat-label> | ||
196 | - <mat-datetimepicker-toggle [for]="endTimePicker5" matPrefix></mat-datetimepicker-toggle> | ||
197 | - <mat-datetimepicker #endTimePicker5 type="time" openOnFocus="true"></mat-datetimepicker> | ||
198 | - <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker5"> | ||
199 | - </mat-form-field> | ||
200 | - </div> | ||
201 | - <div fxFlex fxLayoutAlign="center center" | ||
202 | - style="text-align: center" | ||
203 | - [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(4))"> | ||
204 | - </div> | ||
205 | - </div> | ||
206 | - <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="5" fxLayoutAlign="start center" fxLayoutAlign.xs="center start"> | ||
207 | - <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 5)"> | ||
208 | - {{ 'device-profile.schedule-day.saturday' | translate }} | ||
209 | - </mat-checkbox> | ||
210 | - <div fxLayout="row" fxLayoutGap="8px" fxFlex> | ||
211 | - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> | ||
212 | - <mat-label translate>device-profile.schedule-time-from</mat-label> | ||
213 | - <mat-datetimepicker-toggle [for]="startTimePicker6" matPrefix></mat-datetimepicker-toggle> | ||
214 | - <mat-datetimepicker #startTimePicker6 type="time" openOnFocus="true"></mat-datetimepicker> | ||
215 | - <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker6"> | ||
216 | - </mat-form-field> | ||
217 | - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> | ||
218 | - <mat-label translate>device-profile.schedule-time-to</mat-label> | ||
219 | - <mat-datetimepicker-toggle [for]="endTimePicker6" matPrefix></mat-datetimepicker-toggle> | ||
220 | - <mat-datetimepicker #endTimePicker6 type="time" openOnFocus="true"></mat-datetimepicker> | ||
221 | - <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker6"> | ||
222 | - </mat-form-field> | ||
223 | - </div> | ||
224 | - <div fxFlex fxLayoutAlign="center center" | ||
225 | - style="text-align: center" | ||
226 | - [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(5))"> | ||
227 | - </div> | ||
228 | - </div> | ||
229 | - <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="6" fxLayoutAlign="start center" fxLayoutAlign.xs="center start"> | ||
230 | - <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 6)"> | ||
231 | - {{ 'device-profile.schedule-day.sunday' | translate }} | 77 | + |
78 | + <div *ngFor="let day of allDays" fxLayout="column" formArrayName="items" fxLayoutGap="1em"> | ||
79 | + <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" [formGroupName]="''+day" fxLayoutAlign="start center" fxLayoutAlign.xs="center start"> | ||
80 | + <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, day)"> | ||
81 | + {{ dayOfWeekTranslationsArray[day] | translate }} | ||
232 | </mat-checkbox> | 82 | </mat-checkbox> |
233 | <div fxLayout="row" fxLayoutGap="8px" fxFlex> | 83 | <div fxLayout="row" fxLayoutGap="8px" fxFlex> |
234 | <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> | 84 | <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> |
235 | <mat-label translate>device-profile.schedule-time-from</mat-label> | 85 | <mat-label translate>device-profile.schedule-time-from</mat-label> |
236 | - <mat-datetimepicker-toggle [for]="startTimePicker7" matPrefix></mat-datetimepicker-toggle> | ||
237 | - <mat-datetimepicker #startTimePicker7 type="time" openOnFocus="true"></mat-datetimepicker> | ||
238 | - <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker7"> | 86 | + <mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle> |
87 | + <mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker> | ||
88 | + <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker"> | ||
239 | </mat-form-field> | 89 | </mat-form-field> |
240 | <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> | 90 | <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> |
241 | <mat-label translate>device-profile.schedule-time-to</mat-label> | 91 | <mat-label translate>device-profile.schedule-time-to</mat-label> |
242 | - <mat-datetimepicker-toggle [for]="endTimePicker7" matPrefix></mat-datetimepicker-toggle> | ||
243 | - <mat-datetimepicker #endTimePicker7 type="time" openOnFocus="true"></mat-datetimepicker> | ||
244 | - <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker7"> | 92 | + <mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle> |
93 | + <mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker> | ||
94 | + <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker"> | ||
245 | </mat-form-field> | 95 | </mat-form-field> |
246 | </div> | 96 | </div> |
247 | <div fxFlex fxLayoutAlign="center center" | 97 | <div fxFlex fxLayoutAlign="center center" |
248 | style="text-align: center" | 98 | style="text-align: center" |
249 | - [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(6))"> | 99 | + [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))"> |
250 | </div> | 100 | </div> |
251 | </div> | 101 | </div> |
252 | </div> | 102 | </div> |
103 | + | ||
104 | + <tb-error style="display: block;" [error]="alarmScheduleForm.get('items').hasError('dayOfWeeks') | ||
105 | + ? ('device-profile.schedule-days-of-week-required' | translate) : ''"></tb-error> | ||
253 | </section> | 106 | </section> |
254 | </div> | 107 | </div> |
255 | </section> | 108 | </section> |
@@ -28,7 +28,12 @@ import { | @@ -28,7 +28,12 @@ import { | ||
28 | Validator, | 28 | Validator, |
29 | Validators | 29 | Validators |
30 | } from '@angular/forms'; | 30 | } from '@angular/forms'; |
31 | -import { AlarmSchedule, AlarmScheduleType, AlarmScheduleTypeTranslationMap } from '@shared/models/device.models'; | 31 | +import { |
32 | + AlarmSchedule, | ||
33 | + AlarmScheduleType, | ||
34 | + AlarmScheduleTypeTranslationMap, | ||
35 | + dayOfWeekTranslations, getAlarmScheduleRangeText, timeOfDayToUTCTimestamp, utcTimestampToTimeOfDay | ||
36 | +} from '@shared/models/device.models'; | ||
32 | import { isDefined, isDefinedAndNotNull } from '@core/utils'; | 37 | import { isDefined, isDefinedAndNotNull } from '@core/utils'; |
33 | import * as _moment from 'moment-timezone'; | 38 | import * as _moment from 'moment-timezone'; |
34 | import { MatCheckboxChange } from '@angular/material/checkbox'; | 39 | import { MatCheckboxChange } from '@angular/material/checkbox'; |
@@ -59,11 +64,18 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | @@ -59,11 +64,18 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | ||
59 | alarmScheduleType = AlarmScheduleType; | 64 | alarmScheduleType = AlarmScheduleType; |
60 | alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap; | 65 | alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap; |
61 | 66 | ||
67 | + dayOfWeekTranslationsArray = dayOfWeekTranslations; | ||
68 | + | ||
69 | + allDays = Array(7).fill(0).map((x, i) => i); | ||
70 | + | ||
71 | + firstRowDays = Array(4).fill(0).map((x, i) => i); | ||
72 | + secondRowDays = Array(3).fill(0).map((x, i) => i + 4); | ||
73 | + | ||
62 | private modelValue: AlarmSchedule; | 74 | private modelValue: AlarmSchedule; |
63 | 75 | ||
64 | private defaultItems = Array.from({length: 7}, (value, i) => ({ | 76 | private defaultItems = Array.from({length: 7}, (value, i) => ({ |
65 | enabled: true, | 77 | enabled: true, |
66 | - dayOfWeek: i | 78 | + dayOfWeek: i + 1 |
67 | })); | 79 | })); |
68 | 80 | ||
69 | private propagateChange = (v: any) => { }; | 81 | private propagateChange = (v: any) => { }; |
@@ -75,10 +87,10 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | @@ -75,10 +87,10 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | ||
75 | this.alarmScheduleForm = this.fb.group({ | 87 | this.alarmScheduleForm = this.fb.group({ |
76 | type: [AlarmScheduleType.ANY_TIME, Validators.required], | 88 | type: [AlarmScheduleType.ANY_TIME, Validators.required], |
77 | timezone: [null, Validators.required], | 89 | timezone: [null, Validators.required], |
78 | - daysOfWeek: this.fb.array(new Array(7).fill(false)), | 90 | + daysOfWeek: this.fb.array(new Array(7).fill(false), this.validateDayOfWeeks), |
79 | startsOn: [0, Validators.required], | 91 | startsOn: [0, Validators.required], |
80 | endsOn: [0, Validators.required], | 92 | endsOn: [0, Validators.required], |
81 | - items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i))) | 93 | + items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems) |
82 | }); | 94 | }); |
83 | this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => { | 95 | this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => { |
84 | this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: this.defaultTimezone}, {emitEvent: false}); | 96 | this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: this.defaultTimezone}, {emitEvent: false}); |
@@ -90,6 +102,26 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | @@ -90,6 +102,26 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | ||
90 | }); | 102 | }); |
91 | } | 103 | } |
92 | 104 | ||
105 | + validateDayOfWeeks(control: AbstractControl): ValidationErrors | null { | ||
106 | + const dayOfWeeks: boolean[] = control.value; | ||
107 | + if (!dayOfWeeks || !dayOfWeeks.length || !dayOfWeeks.find(v => v === true)) { | ||
108 | + return { | ||
109 | + dayOfWeeks: true | ||
110 | + }; | ||
111 | + } | ||
112 | + return null; | ||
113 | + } | ||
114 | + | ||
115 | + validateItems(control: AbstractControl): ValidationErrors | null { | ||
116 | + const items: any[] = control.value; | ||
117 | + if (!items || !items.length || !items.find(v => v.enabled === true)) { | ||
118 | + return { | ||
119 | + dayOfWeeks: true | ||
120 | + }; | ||
121 | + } | ||
122 | + return null; | ||
123 | + } | ||
124 | + | ||
93 | registerOnChange(fn: any): void { | 125 | registerOnChange(fn: any): void { |
94 | this.propagateChange = fn; | 126 | this.propagateChange = fn; |
95 | } | 127 | } |
@@ -123,8 +155,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | @@ -123,8 +155,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | ||
123 | type: this.modelValue.type, | 155 | type: this.modelValue.type, |
124 | timezone: this.modelValue.timezone, | 156 | timezone: this.modelValue.timezone, |
125 | daysOfWeek, | 157 | daysOfWeek, |
126 | - startsOn: this.timestampToTime(this.modelValue.startsOn), | ||
127 | - endsOn: this.timestampToTime(this.modelValue.endsOn) | 158 | + startsOn: utcTimestampToTimeOfDay(this.modelValue.startsOn), |
159 | + endsOn: utcTimestampToTimeOfDay(this.modelValue.endsOn) | ||
128 | }, {emitEvent: false}); | 160 | }, {emitEvent: false}); |
129 | break; | 161 | break; |
130 | case AlarmScheduleType.CUSTOM: | 162 | case AlarmScheduleType.CUSTOM: |
@@ -136,8 +168,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | @@ -136,8 +168,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | ||
136 | this.disabledSelectedTime(item.enabled, index); | 168 | this.disabledSelectedTime(item.enabled, index); |
137 | alarmDays.push({ | 169 | alarmDays.push({ |
138 | enabled: item.enabled, | 170 | enabled: item.enabled, |
139 | - startsOn: this.timestampToTime(item.startsOn), | ||
140 | - endsOn: this.timestampToTime(item.endsOn) | 171 | + startsOn: utcTimestampToTimeOfDay(item.startsOn), |
172 | + endsOn: utcTimestampToTimeOfDay(item.endsOn) | ||
141 | }); | 173 | }); |
142 | }); | 174 | }); |
143 | this.alarmScheduleForm.patchValue({ | 175 | this.alarmScheduleForm.patchValue({ |
@@ -202,15 +234,15 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | @@ -202,15 +234,15 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | ||
202 | .filter(day => !!day); | 234 | .filter(day => !!day); |
203 | } | 235 | } |
204 | if (isDefined(value.startsOn) && value.startsOn !== 0) { | 236 | if (isDefined(value.startsOn) && value.startsOn !== 0) { |
205 | - value.startsOn = this.timeToTimestampUTC(value.startsOn); | 237 | + value.startsOn = timeOfDayToUTCTimestamp(value.startsOn); |
206 | } | 238 | } |
207 | if (isDefined(value.endsOn) && value.endsOn !== 0) { | 239 | if (isDefined(value.endsOn) && value.endsOn !== 0) { |
208 | - value.endsOn = this.timeToTimestampUTC(value.endsOn); | 240 | + value.endsOn = timeOfDayToUTCTimestamp(value.endsOn); |
209 | } | 241 | } |
210 | if (isDefined(value.items)){ | 242 | if (isDefined(value.items)){ |
211 | value.items = this.alarmScheduleForm.getRawValue().items; | 243 | value.items = this.alarmScheduleForm.getRawValue().items; |
212 | value.items = value.items.map((item) => { | 244 | value.items = value.items.map((item) => { |
213 | - return { ...item, startsOn: this.timeToTimestampUTC(item.startsOn), endsOn: this.timeToTimestampUTC(item.endsOn)}; | 245 | + return { ...item, startsOn: timeOfDayToUTCTimestamp(item.startsOn), endsOn: timeOfDayToUTCTimestamp(item.endsOn)}; |
214 | }); | 246 | }); |
215 | } | 247 | } |
216 | this.modelValue = value; | 248 | this.modelValue = value; |
@@ -218,21 +250,11 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | @@ -218,21 +250,11 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | ||
218 | } | 250 | } |
219 | } | 251 | } |
220 | 252 | ||
221 | - private timeToTimestampUTC(date: Date | number): number { | ||
222 | - if (typeof date === 'number' || date === null) { | ||
223 | - return 0; | ||
224 | - } | ||
225 | - return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf(); | ||
226 | - } | ||
227 | - | ||
228 | - private timestampToTime(time = 0): Date { | ||
229 | - return new Date(time + new Date().getTimezoneOffset() * 60 * 1000); | ||
230 | - } | ||
231 | 253 | ||
232 | private defaultItemsScheduler(index): FormGroup { | 254 | private defaultItemsScheduler(index): FormGroup { |
233 | return this.fb.group({ | 255 | return this.fb.group({ |
234 | enabled: [true], | 256 | enabled: [true], |
235 | - dayOfWeek: [index], | 257 | + dayOfWeek: [index + 1], |
236 | startsOn: [0, Validators.required], | 258 | startsOn: [0, Validators.required], |
237 | endsOn: [0, Validators.required] | 259 | endsOn: [0, Validators.required] |
238 | }); | 260 | }); |
@@ -253,23 +275,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | @@ -253,23 +275,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, | ||
253 | } | 275 | } |
254 | } | 276 | } |
255 | 277 | ||
256 | - private timeToMoment(date: Date | number): _moment.Moment { | ||
257 | - if (typeof date === 'number' || date === null) { | ||
258 | - return _moment([1970, 0, 1, 0, 0, 0, 0]); | ||
259 | - } | ||
260 | - return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 0, 0]); | ||
261 | - } | ||
262 | - | ||
263 | getSchedulerRangeText(control: FormGroup | AbstractControl): string { | 278 | getSchedulerRangeText(control: FormGroup | AbstractControl): string { |
264 | - const start = this.timeToMoment(control.get('startsOn').value); | ||
265 | - const end = this.timeToMoment(control.get('endsOn').value); | ||
266 | - if (start < end) { | ||
267 | - return `<span><span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">${end.format('hh:mm A')}</span></span>`; | ||
268 | - } else if (start.valueOf() === 0 && end.valueOf() === 0 || start.isSame(_moment([1970, 0])) && end.isSame(_moment([1970, 0]))) { | ||
269 | - return '<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">12:00 PM</span></span>'; | ||
270 | - } | ||
271 | - return `<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">${end.format('hh:mm A')}</span>` + | ||
272 | - ` and <span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">12:00 PM</span></span>`; | 279 | + return getAlarmScheduleRangeText(control.get('startsOn').value, control.get('endsOn').value); |
273 | } | 280 | } |
274 | 281 | ||
275 | get itemsSchedulerForm(): FormArray { | 282 | get itemsSchedulerForm(): FormArray { |
@@ -56,7 +56,7 @@ | @@ -56,7 +56,7 @@ | ||
56 | <mat-checkbox formControlName="propagate" style="display: block; padding-bottom: 16px;"> | 56 | <mat-checkbox formControlName="propagate" style="display: block; padding-bottom: 16px;"> |
57 | {{ 'device-profile.propagate-alarm' | translate }} | 57 | {{ 'device-profile.propagate-alarm' | translate }} |
58 | </mat-checkbox> | 58 | </mat-checkbox> |
59 | - <section *ngIf="alarmFormGroup.get('propagate').value === true"> | 59 | + <section *ngIf="alarmFormGroup.get('propagate').value === true" style="padding-bottom: 1em;"> |
60 | <mat-form-field floatLabel="always" class="mat-block"> | 60 | <mat-form-field floatLabel="always" class="mat-block"> |
61 | <mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label> | 61 | <mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label> |
62 | <mat-chip-list #relationTypesChipList [disabled]="disabled"> | 62 | <mat-chip-list #relationTypesChipList [disabled]="disabled"> |
@@ -57,6 +57,7 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit | @@ -57,6 +57,7 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit | ||
57 | 57 | ||
58 | separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; | 58 | separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; |
59 | 59 | ||
60 | + @Input() | ||
60 | expanded = false; | 61 | expanded = false; |
61 | 62 | ||
62 | private modelValue: DeviceProfileAlarm; | 63 | private modelValue: DeviceProfileAlarm; |
@@ -21,6 +21,7 @@ | @@ -21,6 +21,7 @@ | ||
21 | let $index = index; last as isLast;" | 21 | let $index = index; last as isLast;" |
22 | fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}"> | 22 | fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}"> |
23 | <tb-device-profile-alarm [formControl]="alarmControl" | 23 | <tb-device-profile-alarm [formControl]="alarmControl" |
24 | + [expanded]="$index === 0" | ||
24 | (removeAlarm)="removeAlarm($index)"> | 25 | (removeAlarm)="removeAlarm($index)"> |
25 | </tb-device-profile-alarm> | 26 | </tb-device-profile-alarm> |
26 | </div> | 27 | </div> |
@@ -29,7 +30,7 @@ | @@ -29,7 +30,7 @@ | ||
29 | <span translate fxLayoutAlign="center center" | 30 | <span translate fxLayoutAlign="center center" |
30 | class="tb-prompt">device-profile.no-alarm-rules</span> | 31 | class="tb-prompt">device-profile.no-alarm-rules</span> |
31 | </div> | 32 | </div> |
32 | - <div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="end center" | 33 | + <div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="start center" |
33 | style="padding-top: 16px;"> | 34 | style="padding-top: 16px;"> |
34 | <button mat-raised-button color="primary" | 35 | <button mat-raised-button color="primary" |
35 | type="button" | 36 | type="button" |
ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html
0 → 100644
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2020 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 | +<form [formGroup]="editDetailsFormGroup" (ngSubmit)="save()" style="width: 800px;"> | ||
19 | + <mat-toolbar color="primary"> | ||
20 | + <h2>{{ 'device-profile.alarm-rule-details' | translate }}</h2> | ||
21 | + <span fxFlex></span> | ||
22 | + <button mat-icon-button | ||
23 | + (click)="cancel()" | ||
24 | + type="button"> | ||
25 | + <mat-icon class="material-icons">close</mat-icon> | ||
26 | + </button> | ||
27 | + </mat-toolbar> | ||
28 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
29 | + </mat-progress-bar> | ||
30 | + <div mat-dialog-content> | ||
31 | + <fieldset [disabled]="isLoading$ | async"> | ||
32 | + <div fxFlex fxLayout="column"> | ||
33 | + <mat-form-field class="mat-block"> | ||
34 | + <mat-label translate>device-profile.alarm-details</mat-label> | ||
35 | + <textarea matInput formControlName="alarmDetails" rows="5"></textarea> | ||
36 | + </mat-form-field> | ||
37 | + </div> | ||
38 | + </fieldset> | ||
39 | + </div> | ||
40 | + <div mat-dialog-actions fxLayoutAlign="end center"> | ||
41 | + <button mat-raised-button color="primary" | ||
42 | + type="submit" | ||
43 | + [disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty"> | ||
44 | + {{ 'action.save' | translate }} | ||
45 | + </button> | ||
46 | + <button mat-button color="primary" | ||
47 | + type="button" | ||
48 | + [disabled]="(isLoading$ | async)" | ||
49 | + (click)="cancel()" cdkFocusInitial> | ||
50 | + {{ 'action.cancel' | translate }} | ||
51 | + </button> | ||
52 | + </div> | ||
53 | +</form> |
ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts
0 → 100644
1 | +/// | ||
2 | +/// Copyright © 2016-2020 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, Inject, OnInit, SkipSelf } from '@angular/core'; | ||
18 | +import { ErrorStateMatcher } from '@angular/material/core'; | ||
19 | +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; | ||
20 | +import { Store } from '@ngrx/store'; | ||
21 | +import { AppState } from '@core/core.state'; | ||
22 | +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; | ||
23 | +import { Router } from '@angular/router'; | ||
24 | +import { DialogComponent } from '@app/shared/components/dialog.component'; | ||
25 | +import { UtilsService } from '@core/services/utils.service'; | ||
26 | +import { TranslateService } from '@ngx-translate/core'; | ||
27 | + | ||
28 | +export interface EditAlarmDetailsDialogData { | ||
29 | + alarmDetails: string; | ||
30 | +} | ||
31 | + | ||
32 | +@Component({ | ||
33 | + selector: 'tb-edit-alarm-details-dialog', | ||
34 | + templateUrl: './edit-alarm-details-dialog.component.html', | ||
35 | + providers: [{provide: ErrorStateMatcher, useExisting: EditAlarmDetailsDialogComponent}], | ||
36 | + styleUrls: [] | ||
37 | +}) | ||
38 | +export class EditAlarmDetailsDialogComponent extends DialogComponent<EditAlarmDetailsDialogComponent, string> | ||
39 | + implements OnInit, ErrorStateMatcher { | ||
40 | + | ||
41 | + alarmDetails = this.data.alarmDetails; | ||
42 | + | ||
43 | + editDetailsFormGroup: FormGroup; | ||
44 | + | ||
45 | + submitted = false; | ||
46 | + | ||
47 | + constructor(protected store: Store<AppState>, | ||
48 | + protected router: Router, | ||
49 | + @Inject(MAT_DIALOG_DATA) public data: EditAlarmDetailsDialogData, | ||
50 | + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | ||
51 | + public dialogRef: MatDialogRef<EditAlarmDetailsDialogComponent, string>, | ||
52 | + private fb: FormBuilder, | ||
53 | + private utils: UtilsService, | ||
54 | + public translate: TranslateService) { | ||
55 | + super(store, router, dialogRef); | ||
56 | + | ||
57 | + this.editDetailsFormGroup = this.fb.group({ | ||
58 | + alarmDetails: [this.alarmDetails] | ||
59 | + }); | ||
60 | + } | ||
61 | + | ||
62 | + ngOnInit(): void { | ||
63 | + } | ||
64 | + | ||
65 | + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | ||
66 | + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); | ||
67 | + const customErrorState = !!(control && control.invalid && this.submitted); | ||
68 | + return originalErrorState || customErrorState; | ||
69 | + } | ||
70 | + | ||
71 | + cancel(): void { | ||
72 | + this.dialogRef.close(null); | ||
73 | + } | ||
74 | + | ||
75 | + save(): void { | ||
76 | + this.submitted = true; | ||
77 | + this.alarmDetails = this.editDetailsFormGroup.get('alarmDetails').value; | ||
78 | + this.dialogRef.close(this.alarmDetails); | ||
79 | + } | ||
80 | +} |
@@ -266,11 +266,11 @@ export class DeviceWizardDialogComponent extends | @@ -266,11 +266,11 @@ export class DeviceWizardDialogComponent extends | ||
266 | }) | 266 | }) |
267 | ); | 267 | ); |
268 | } else { | 268 | } else { |
269 | - return of(null); | 269 | + return of(this.deviceWizardFormGroup.get('deviceProfileId').value); |
270 | } | 270 | } |
271 | } | 271 | } |
272 | 272 | ||
273 | - private createDevice(profileId: EntityId = this.deviceWizardFormGroup.get('deviceProfileId').value): Observable<BaseData<HasId>> { | 273 | + private createDevice(profileId): Observable<BaseData<HasId>> { |
274 | const device = { | 274 | const device = { |
275 | name: this.deviceWizardFormGroup.get('name').value, | 275 | name: this.deviceWizardFormGroup.get('name').value, |
276 | label: this.deviceWizardFormGroup.get('label').value, | 276 | label: this.deviceWizardFormGroup.get('label').value, |
@@ -61,29 +61,7 @@ | @@ -61,29 +61,7 @@ | ||
61 | </div> | 61 | </div> |
62 | </div> | 62 | </div> |
63 | </mat-tab> | 63 | </mat-tab> |
64 | -<mat-tab *ngIf="entity && !isEdit" | ||
65 | - label="{{ 'attribute.attributes' | translate }}" #attributesTab="matTab"> | ||
66 | - <tb-attribute-table [defaultAttributeScope]="attributeScopes.SERVER_SCOPE" | ||
67 | - [active]="attributesTab.isActive" | ||
68 | - [entityId]="entity.id" | ||
69 | - [entityName]="entity.name"> | ||
70 | - </tb-attribute-table> | ||
71 | -</mat-tab> | ||
72 | -<mat-tab *ngIf="entity && !isEdit" | ||
73 | - label="{{ 'attribute.latest-telemetry' | translate }}" #telemetryTab="matTab"> | ||
74 | - <tb-attribute-table [defaultAttributeScope]="latestTelemetryTypes.LATEST_TELEMETRY" | ||
75 | - disableAttributeScopeSelection | ||
76 | - [active]="telemetryTab.isActive" | ||
77 | - [entityId]="entity.id" | ||
78 | - [entityName]="entity.name"> | ||
79 | - </tb-attribute-table> | ||
80 | -</mat-tab> | ||
81 | -<mat-tab *ngIf="entity && !isEdit" | ||
82 | - label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab"> | ||
83 | - <tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table> | ||
84 | -</mat-tab> | ||
85 | -<mat-tab *ngIf="entity && !isEdit" | ||
86 | - label="{{ 'tenant.events' | translate }}" #eventsTab="matTab"> | ||
87 | - <tb-event-table [defaultEventType]="eventTypes.ERROR" [active]="eventsTab.isActive" [tenantId]="nullUid" | ||
88 | - [entityId]="entity.id"></tb-event-table> | 64 | +<mat-tab *ngIf="entity" |
65 | + label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab"> | ||
66 | + <tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table> | ||
89 | </mat-tab> | 67 | </mat-tab> |
@@ -25,6 +25,8 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id'; | @@ -25,6 +25,8 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id'; | ||
25 | import { EntityInfoData } from '@shared/models/entity.models'; | 25 | import { EntityInfoData } from '@shared/models/entity.models'; |
26 | import { KeyFilter } from '@shared/models/query/query.models'; | 26 | import { KeyFilter } from '@shared/models/query/query.models'; |
27 | import { TimeUnit } from '@shared/models/time/time.models'; | 27 | import { TimeUnit } from '@shared/models/time/time.models'; |
28 | +import * as _moment from 'moment-timezone'; | ||
29 | +import { AbstractControl, FormGroup } from '@angular/forms'; | ||
28 | 30 | ||
29 | export enum DeviceProfileType { | 31 | export enum DeviceProfileType { |
30 | DEFAULT = 'DEFAULT' | 32 | DEFAULT = 'DEFAULT' |
@@ -408,3 +410,62 @@ export interface ClaimResult { | @@ -408,3 +410,62 @@ export interface ClaimResult { | ||
408 | device: Device; | 410 | device: Device; |
409 | response: ClaimResponse; | 411 | response: ClaimResponse; |
410 | } | 412 | } |
413 | + | ||
414 | +export const dayOfWeekTranslations = new Array<string>( | ||
415 | + 'device-profile.schedule-day.monday', | ||
416 | + 'device-profile.schedule-day.tuesday', | ||
417 | + 'device-profile.schedule-day.wednesday', | ||
418 | + 'device-profile.schedule-day.thursday', | ||
419 | + 'device-profile.schedule-day.friday', | ||
420 | + 'device-profile.schedule-day.saturday', | ||
421 | + 'device-profile.schedule-day.sunday' | ||
422 | +); | ||
423 | + | ||
424 | +export function getDayString(day: number): string { | ||
425 | + switch (day) { | ||
426 | + case 0: | ||
427 | + return 'device-profile.schedule-day.monday'; | ||
428 | + case 1: | ||
429 | + return this.translate.instant('device-profile.schedule-day.tuesday'); | ||
430 | + case 2: | ||
431 | + return this.translate.instant('device-profile.schedule-day.wednesday'); | ||
432 | + case 3: | ||
433 | + return this.translate.instant('device-profile.schedule-day.thursday'); | ||
434 | + case 4: | ||
435 | + return this.translate.instant('device-profile.schedule-day.friday'); | ||
436 | + case 5: | ||
437 | + return this.translate.instant('device-profile.schedule-day.saturday'); | ||
438 | + case 6: | ||
439 | + return this.translate.instant('device-profile.schedule-day.sunday'); | ||
440 | + } | ||
441 | +} | ||
442 | + | ||
443 | +export function timeOfDayToUTCTimestamp(date: Date | number): number { | ||
444 | + if (typeof date === 'number' || date === null) { | ||
445 | + return 0; | ||
446 | + } | ||
447 | + return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf(); | ||
448 | +} | ||
449 | + | ||
450 | +export function utcTimestampToTimeOfDay(time = 0): Date { | ||
451 | + return new Date(time + new Date().getTimezoneOffset() * 60 * 1000); | ||
452 | +} | ||
453 | + | ||
454 | +function timeOfDayToMoment(date: Date | number): _moment.Moment { | ||
455 | + if (typeof date === 'number' || date === null) { | ||
456 | + return _moment([1970, 0, 1, 0, 0, 0, 0]); | ||
457 | + } | ||
458 | + return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 0, 0]); | ||
459 | +} | ||
460 | + | ||
461 | +export function getAlarmScheduleRangeText(startsOn: Date | number, endsOn: Date | number): string { | ||
462 | + const start = timeOfDayToMoment(startsOn); | ||
463 | + const end = timeOfDayToMoment(endsOn); | ||
464 | + if (start < end) { | ||
465 | + return `<span><span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">${end.format('hh:mm A')}</span></span>`; | ||
466 | + } else if (start.valueOf() === 0 && end.valueOf() === 0 || start.isSame(_moment([1970, 0])) && end.isSame(_moment([1970, 0]))) { | ||
467 | + return '<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">12:00 PM</span></span>'; | ||
468 | + } | ||
469 | + return `<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">${end.format('hh:mm A')}</span>` + | ||
470 | + ` and <span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">12:00 PM</span></span>`; | ||
471 | +} |
@@ -476,21 +476,23 @@ export function keyFilterInfosToKeyFilters(keyFilterInfos: Array<KeyFilterInfo>) | @@ -476,21 +476,23 @@ export function keyFilterInfosToKeyFilters(keyFilterInfos: Array<KeyFilterInfo>) | ||
476 | export function keyFiltersToKeyFilterInfos(keyFilters: Array<KeyFilter>): Array<KeyFilterInfo> { | 476 | export function keyFiltersToKeyFilterInfos(keyFilters: Array<KeyFilter>): Array<KeyFilterInfo> { |
477 | const keyFilterInfos: Array<KeyFilterInfo> = []; | 477 | const keyFilterInfos: Array<KeyFilterInfo> = []; |
478 | const keyFilterInfoMap: {[infoKey: string]: KeyFilterInfo} = {}; | 478 | const keyFilterInfoMap: {[infoKey: string]: KeyFilterInfo} = {}; |
479 | - for (const keyFilter of keyFilters) { | ||
480 | - const key = keyFilter.key; | ||
481 | - const infoKey = key.key + key.type + keyFilter.valueType; | ||
482 | - let keyFilterInfo = keyFilterInfoMap[infoKey]; | ||
483 | - if (!keyFilterInfo) { | ||
484 | - keyFilterInfo = { | ||
485 | - key, | ||
486 | - valueType: keyFilter.valueType, | ||
487 | - predicates: [] | ||
488 | - }; | ||
489 | - keyFilterInfoMap[infoKey] = keyFilterInfo; | ||
490 | - keyFilterInfos.push(keyFilterInfo); | ||
491 | - } | ||
492 | - if (keyFilter.predicate) { | ||
493 | - keyFilterInfo.predicates.push(keyFilterPredicateToKeyFilterPredicateInfo(keyFilter.predicate)); | 479 | + if (keyFilters) { |
480 | + for (const keyFilter of keyFilters) { | ||
481 | + const key = keyFilter.key; | ||
482 | + const infoKey = key.key + key.type + keyFilter.valueType; | ||
483 | + let keyFilterInfo = keyFilterInfoMap[infoKey]; | ||
484 | + if (!keyFilterInfo) { | ||
485 | + keyFilterInfo = { | ||
486 | + key, | ||
487 | + valueType: keyFilter.valueType, | ||
488 | + predicates: [] | ||
489 | + }; | ||
490 | + keyFilterInfoMap[infoKey] = keyFilterInfo; | ||
491 | + keyFilterInfos.push(keyFilterInfo); | ||
492 | + } | ||
493 | + if (keyFilter.predicate) { | ||
494 | + keyFilterInfo.predicates.push(keyFilterPredicateToKeyFilterPredicateInfo(keyFilter.predicate)); | ||
495 | + } | ||
494 | } | 496 | } |
495 | } | 497 | } |
496 | return keyFilterInfos; | 498 | return keyFilterInfos; |
@@ -55,7 +55,9 @@ | @@ -55,7 +55,9 @@ | ||
55 | "continue": "Continue", | 55 | "continue": "Continue", |
56 | "discard-changes": "Discard Changes", | 56 | "discard-changes": "Discard Changes", |
57 | "download": "Download", | 57 | "download": "Download", |
58 | - "next-with-label": "Next: {{label}}" | 58 | + "next-with-label": "Next: {{label}}", |
59 | + "read-more": "Read more", | ||
60 | + "hide": "Hide" | ||
59 | }, | 61 | }, |
60 | "aggregation": { | 62 | "aggregation": { |
61 | "aggregation": "Aggregation", | 63 | "aggregation": "Aggregation", |
@@ -932,15 +934,18 @@ | @@ -932,15 +934,18 @@ | ||
932 | "condition-type": "Condition type", | 934 | "condition-type": "Condition type", |
933 | "condition-type-simple": "Simple", | 935 | "condition-type-simple": "Simple", |
934 | "condition-type-duration": "Duration", | 936 | "condition-type-duration": "Duration", |
937 | + "condition-during": "During <b>{{during}}</b>", | ||
935 | "condition-type-repeating": "Repeating", | 938 | "condition-type-repeating": "Repeating", |
936 | "condition-type-required": "Condition type is required.", | 939 | "condition-type-required": "Condition type is required.", |
937 | "condition-repeating-value": "Count of events", | 940 | "condition-repeating-value": "Count of events", |
938 | "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", | 941 | "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", |
939 | "condition-repeating-value-pattern": "Count of events should be integers.", | 942 | "condition-repeating-value-pattern": "Count of events should be integers.", |
940 | "condition-repeating-value-required": "Count of events is required.", | 943 | "condition-repeating-value-required": "Count of events is required.", |
944 | + "condition-repeat-times": "Repeats <b>{ count, plural, 1 {1 time} other {# times} }</b>", | ||
941 | "schedule-type": "Scheduler type", | 945 | "schedule-type": "Scheduler type", |
942 | "schedule-type-required": "Scheduler type is required.", | 946 | "schedule-type-required": "Scheduler type is required.", |
943 | "schedule": "Schedule", | 947 | "schedule": "Schedule", |
948 | + "edit-schedule": "Edit alarm schedule", | ||
944 | "schedule-any-time": "Active all the time", | 949 | "schedule-any-time": "Active all the time", |
945 | "schedule-specific-time": "Active at a specific time", | 950 | "schedule-specific-time": "Active at a specific time", |
946 | "schedule-custom": "Custom", | 951 | "schedule-custom": "Custom", |
@@ -956,7 +961,8 @@ | @@ -956,7 +961,8 @@ | ||
956 | "schedule-days": "Days", | 961 | "schedule-days": "Days", |
957 | "schedule-time": "Time", | 962 | "schedule-time": "Time", |
958 | "schedule-time-from": "From", | 963 | "schedule-time-from": "From", |
959 | - "schedule-time-to": "To" | 964 | + "schedule-time-to": "To", |
965 | + "schedule-days-of-week-required": "At least one day of week should be selected." | ||
960 | }, | 966 | }, |
961 | "dialog": { | 967 | "dialog": { |
962 | "close": "Close dialog" | 968 | "close": "Close dialog" |