Browse Source

Merge remote-tracking branch 'upstream/master' into improvement/oauth2

pull/3575/head
Vladyslav_Prykhodko 6 years ago
parent
commit
1c2ec66f16
  1. 8
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  2. 2
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  3. 6
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  4. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateService.java
  5. 1
      common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateService.java
  6. 3
      common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValueSourceType.java
  7. 4
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationTemplateDao.java
  8. 9
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java
  9. 11
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java
  10. 4
      dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java
  11. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationTemplateDao.java
  12. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationTemplateRepository.java
  13. 14
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  14. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeStateDao.java
  15. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeStateRepository.java
  16. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  17. 280
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java
  18. 49
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java
  19. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java
  20. 34
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java
  21. 63
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileState.java
  22. 104
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java
  23. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java
  24. 137
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java
  25. 39
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/SnapshotUpdate.java
  26. 29
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java
  27. 19
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  28. 124
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html
  29. 10
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss
  30. 150
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts
  31. 8
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html
  32. 11
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss
  33. 85
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts
  34. 118
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html
  35. 24
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss
  36. 93
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts
  37. 15
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html
  38. 34
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.ts
  39. 33
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.html
  40. 34
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.scss
  41. 137
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts
  42. 191
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html
  43. 83
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts
  44. 1
      ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss
  45. 2
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html
  46. 1
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts
  47. 3
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html
  48. 53
      ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html
  49. 80
      ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts
  50. 4
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts
  51. 28
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html
  52. 61
      ui-ngx/src/app/shared/models/device.models.ts
  53. 32
      ui-ngx/src/app/shared/models/query/query.models.ts
  54. 10
      ui-ngx/src/assets/locale/locale.constant-en_US.json

8
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -469,6 +469,14 @@ class DefaultTbContext implements TbContext {
return mainCtx.getRuleNodeStateService().save(getTenantId(), state); return mainCtx.getRuleNodeStateService().save(getTenantId(), state);
} }
@Override
public void clearRuleNodeStates() {
if (log.isDebugEnabled()) {
log.debug("[{}][{}] Going to clear rule node states", getTenantId(), getSelfId());
}
mainCtx.getRuleNodeStateService().removeByRuleNodeId(getTenantId(), getSelfId());
}
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) { private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ruleNodeId", ruleNodeId.toString()); metaData.putValue("ruleNodeId", ruleNodeId.toString());

2
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -164,6 +164,8 @@ public class RuleChainController extends BaseController {
RuleChain savedRuleChain = installScripts.createDefaultRuleChain(getCurrentUser().getTenantId(), request.getName()); RuleChain savedRuleChain = installScripts.createDefaultRuleChain(getCurrentUser().getTenantId(), request.getName());
tbClusterService.onEntityStateChange(savedRuleChain.getTenantId(), savedRuleChain.getId(), ComponentLifecycleEvent.CREATED);
logEntityAction(savedRuleChain.getId(), savedRuleChain, null, ActionType.ADDED, null); logEntityAction(savedRuleChain.getId(), savedRuleChain, null, ActionType.ADDED, null);
return savedRuleChain; return savedRuleChain;

6
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -42,6 +42,7 @@ import java.nio.file.DirectoryStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Optional;
import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper; import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper;
@ -243,6 +244,11 @@ public class InstallScripts {
try { try {
JsonNode oauth2ConfigTemplateJson = objectMapper.readTree(path.toFile()); JsonNode oauth2ConfigTemplateJson = objectMapper.readTree(path.toFile());
OAuth2ClientRegistrationTemplate clientRegistrationTemplate = objectMapper.treeToValue(oauth2ConfigTemplateJson, OAuth2ClientRegistrationTemplate.class); OAuth2ClientRegistrationTemplate clientRegistrationTemplate = objectMapper.treeToValue(oauth2ConfigTemplateJson, OAuth2ClientRegistrationTemplate.class);
Optional<OAuth2ClientRegistrationTemplate> existingClientRegistrationTemplate =
oAuth2TemplateService.findClientRegistrationTemplateByProviderId(clientRegistrationTemplate.getProviderId());
if (existingClientRegistrationTemplate.isPresent()) {
clientRegistrationTemplate.setId(existingClientRegistrationTemplate.get().getId());
}
oAuth2TemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate); oAuth2TemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate);
} catch (Exception e) { } catch (Exception e) {
log.error("Unable to load oauth2 config templates from json: [{}]", path.toString()); log.error("Unable to load oauth2 config templates from json: [{}]", path.toString());

3
common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateService.java

@ -19,10 +19,13 @@ import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface OAuth2ConfigTemplateService { public interface OAuth2ConfigTemplateService {
OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate); OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate);
Optional<OAuth2ClientRegistrationTemplate> findClientRegistrationTemplateByProviderId(String providerId);
OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId); OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId);
List<OAuth2ClientRegistrationTemplate> findAllClientRegistrationTemplates(); List<OAuth2ClientRegistrationTemplate> findAllClientRegistrationTemplates();

1
common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateService.java

@ -30,4 +30,5 @@ public interface RuleNodeStateService {
RuleNodeState save(TenantId tenantId, RuleNodeState ruleNodeState); RuleNodeState save(TenantId tenantId, RuleNodeState ruleNodeState);
void removeByRuleNodeId(TenantId tenantId, RuleNodeId selfId);
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValueSourceType.java

@ -18,5 +18,6 @@ package org.thingsboard.server.common.data.query;
public enum DynamicValueSourceType { public enum DynamicValueSourceType {
CURRENT_TENANT, CURRENT_TENANT,
CURRENT_CUSTOMER, CURRENT_CUSTOMER,
CURRENT_USER CURRENT_USER,
CURRENT_DEVICE
} }

4
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationTemplateDao.java

@ -19,7 +19,11 @@ import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplat
import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.Dao;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface OAuth2ClientRegistrationTemplateDao extends Dao<OAuth2ClientRegistrationTemplate> { public interface OAuth2ClientRegistrationTemplateDao extends Dao<OAuth2ClientRegistrationTemplate> {
Optional<OAuth2ClientRegistrationTemplate> findByProviderId(String providerId);
List<OAuth2ClientRegistrationTemplate> findAll(); List<OAuth2ClientRegistrationTemplate> findAll();
} }

9
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java

@ -29,6 +29,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import java.util.List; import java.util.List;
import java.util.Optional;
import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateString; import static org.thingsboard.server.dao.service.Validator.validateString;
@ -37,6 +38,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
@Service @Service
public class OAuth2ConfigTemplateServiceImpl extends AbstractEntityService implements OAuth2ConfigTemplateService { public class OAuth2ConfigTemplateServiceImpl extends AbstractEntityService implements OAuth2ConfigTemplateService {
public static final String INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID = "Incorrect clientRegistrationTemplateId "; public static final String INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID = "Incorrect clientRegistrationTemplateId ";
public static final String INCORRECT_CLIENT_REGISTRATION_PROVIDER_ID = "Incorrect clientRegistrationProviderId ";
@Autowired @Autowired
private OAuth2ClientRegistrationTemplateDao clientRegistrationTemplateDao; private OAuth2ClientRegistrationTemplateDao clientRegistrationTemplateDao;
@ -59,6 +61,13 @@ public class OAuth2ConfigTemplateServiceImpl extends AbstractEntityService imple
return savedClientRegistrationTemplate; return savedClientRegistrationTemplate;
} }
@Override
public Optional<OAuth2ClientRegistrationTemplate> findClientRegistrationTemplateByProviderId(String providerId) {
log.trace("Executing findClientRegistrationTemplateByProviderId [{}]", providerId);
validateString(providerId, INCORRECT_CLIENT_REGISTRATION_PROVIDER_ID + providerId);
return clientRegistrationTemplateDao.findByProviderId(providerId);
}
@Override @Override
public OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId) { public OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId) {
log.trace("Executing findClientRegistrationTemplateById [{}]", templateId); log.trace("Executing findClientRegistrationTemplateById [{}]", templateId);

11
dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java

@ -68,6 +68,17 @@ public class BaseRuleNodeStateService extends AbstractEntityService implements R
return saveOrUpdate(tenantId, ruleNodeState, false); return saveOrUpdate(tenantId, ruleNodeState, false);
} }
@Override
public void removeByRuleNodeId(TenantId tenantId, RuleNodeId ruleNodeId) {
if (tenantId == null) {
throw new DataValidationException("Tenant id should be specified!.");
}
if (ruleNodeId == null) {
throw new DataValidationException("Rule node id should be specified!.");
}
ruleNodeStateDao.removeByRuleNodeId(ruleNodeId.getId());
}
public RuleNodeState saveOrUpdate(TenantId tenantId, RuleNodeState ruleNodeState, boolean update) { public RuleNodeState saveOrUpdate(TenantId tenantId, RuleNodeState ruleNodeState, boolean update) {
try { try {
if (update) { if (update) {

4
dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java

@ -16,6 +16,8 @@
package org.thingsboard.server.dao.rule; package org.thingsboard.server.dao.rule;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.rule.RuleNodeState;
@ -31,4 +33,6 @@ public interface RuleNodeStateDao extends Dao<RuleNodeState> {
PageData<RuleNodeState> findByRuleNodeId(UUID ruleNodeId, PageLink pageLink); PageData<RuleNodeState> findByRuleNodeId(UUID ruleNodeId, PageLink pageLink);
RuleNodeState findByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId); RuleNodeState findByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId);
void removeByRuleNodeId(UUID ruleNodeId);
} }

7
dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationTemplateDao.java

@ -26,6 +26,7 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Component @Component
@ -43,6 +44,12 @@ public class JpaOAuth2ClientRegistrationTemplateDao extends JpaAbstractDao<OAuth
return repository; return repository;
} }
@Override
public Optional<OAuth2ClientRegistrationTemplate> findByProviderId(String providerId) {
OAuth2ClientRegistrationTemplate oAuth2ClientRegistrationTemplate = DaoUtil.getData(repository.findByProviderId(providerId));
return Optional.ofNullable(oAuth2ClientRegistrationTemplate);
}
@Override @Override
public List<OAuth2ClientRegistrationTemplate> findAll() { public List<OAuth2ClientRegistrationTemplate> findAll() {
Iterable<OAuth2ClientRegistrationTemplateEntity> entities = repository.findAll(); Iterable<OAuth2ClientRegistrationTemplateEntity> entities = repository.findAll();

3
dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationTemplateRepository.java

@ -21,4 +21,7 @@ import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationTemplateEnti
import java.util.UUID; import java.util.UUID;
public interface OAuth2ClientRegistrationTemplateRepository extends CrudRepository<OAuth2ClientRegistrationTemplateEntity, UUID> { public interface OAuth2ClientRegistrationTemplateRepository extends CrudRepository<OAuth2ClientRegistrationTemplateEntity, UUID> {
OAuth2ClientRegistrationTemplateEntity findByProviderId(String providerId);
} }

14
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java

@ -472,13 +472,13 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
if (entityFilter.isFetchLastLevelOnly()) { if (entityFilter.isFetchLastLevelOnly()) {
String fromOrTo = (entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "from" : "to"); String fromOrTo = (entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "from" : "to");
StringBuilder notExistsPart = new StringBuilder(); StringBuilder notExistsPart = new StringBuilder();
notExistsPart.append(" NOT EXISTS (SELECT 1 from relation nr where ") notExistsPart.append(" NOT EXISTS (SELECT 1 from relation nr ")
.append(whereFilter.replaceAll("re\\.", "nr\\."))
.append(" and ")
.append("nr.").append(fromOrTo).append("_id").append(" = re.").append(toOrFrom).append("_id") .append("nr.").append(fromOrTo).append("_id").append(" = re.").append(toOrFrom).append("_id")
.append(" and ") .append(" and ")
.append("nr.").append(fromOrTo).append("_type").append(" = re.").append(toOrFrom).append("_type"); .append("nr.").append(fromOrTo).append("_type").append(" = re.").append(toOrFrom).append("_type");
if (!StringUtils.isEmpty(entityFilter.getRelationType())) {
notExistsPart.append(" and nr.relation_type = :where_relation_type");
}
notExistsPart.append(")"); notExistsPart.append(")");
whereFilter += " and ( re.lvl = " + entityFilter.getMaxLevel() + " OR " + notExistsPart.toString() + ")"; whereFilter += " and ( re.lvl = " + entityFilter.getMaxLevel() + " OR " + notExistsPart.toString() + ")";
} }
@ -551,12 +551,12 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
StringBuilder notExistsPart = new StringBuilder(); StringBuilder notExistsPart = new StringBuilder();
notExistsPart.append(" NOT EXISTS (SELECT 1 from relation nr WHERE "); notExistsPart.append(" NOT EXISTS (SELECT 1 from relation nr WHERE ");
notExistsPart.append(whereFilter.toString());
notExistsPart notExistsPart
.append(" and ")
.append("nr.").append(fromOrTo).append("_id").append(" = re.").append(toOrFrom).append("_id") .append("nr.").append(fromOrTo).append("_id").append(" = re.").append(toOrFrom).append("_id")
.append(" and ") .append(" and ")
.append("nr.").append(fromOrTo).append("_type").append(" = re.").append(toOrFrom).append("_type"); .append("nr.").append(fromOrTo).append("_type").append(" = re.").append(toOrFrom).append("_type")
.append(" and ")
.append(whereFilter.toString().replaceAll("re\\.", "nr\\."));
notExistsPart.append(")"); notExistsPart.append(")");
whereFilter.append(" and ( re.lvl = ").append(entityFilter.getMaxLevel()).append(" OR ").append(notExistsPart.toString()).append(")"); whereFilter.append(" and ( re.lvl = ").append(entityFilter.getMaxLevel()).append(" OR ").append(notExistsPart.toString()).append(")");

7
dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeStateDao.java

@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
@ -56,4 +57,10 @@ public class JpaRuleNodeStateDao extends JpaAbstractDao<RuleNodeStateEntity, Rul
public RuleNodeState findByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId) { public RuleNodeState findByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId) {
return DaoUtil.getData(ruleNodeStateRepository.findByRuleNodeIdAndEntityId(ruleNodeId, entityId)); return DaoUtil.getData(ruleNodeStateRepository.findByRuleNodeIdAndEntityId(ruleNodeId, entityId));
} }
@Transactional
@Override
public void removeByRuleNodeId(UUID ruleNodeId) {
ruleNodeStateRepository.removeByRuleNodeId(ruleNodeId);
}
} }

3
dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeStateRepository.java

@ -33,4 +33,7 @@ public interface RuleNodeStateRepository extends PagingAndSortingRepository<Rule
@Query("SELECT e FROM RuleNodeStateEntity e WHERE e.ruleNodeId = :ruleNodeId and e.entityId = :entityId") @Query("SELECT e FROM RuleNodeStateEntity e WHERE e.ruleNodeId = :ruleNodeId and e.entityId = :entityId")
RuleNodeStateEntity findByRuleNodeIdAndEntityId(@Param("ruleNodeId") UUID ruleNodeId, @Param("entityId") UUID entityId); RuleNodeStateEntity findByRuleNodeIdAndEntityId(@Param("ruleNodeId") UUID ruleNodeId, @Param("entityId") UUID entityId);
void removeByRuleNodeId(@Param("ruleNodeId") UUID ruleNodeId);
} }

2
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -221,4 +221,6 @@ public interface TbContext {
RuleNodeState findRuleNodeStateForEntity(EntityId entityId); RuleNodeState findRuleNodeStateForEntity(EntityId entityId);
RuleNodeState saveRuleNodeState(RuleNodeState state); RuleNodeState saveRuleNodeState(RuleNodeState state);
void clearRuleNodeStates();
} }

280
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java

@ -29,6 +29,9 @@ import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpe
import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule; import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate; import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.KeyFilterPredicate; import org.thingsboard.server.common.data.query.KeyFilterPredicate;
import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.NumericFilterPredicate;
@ -38,22 +41,25 @@ import org.thingsboard.server.common.msg.tools.SchedulerUtils;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Calendar; import java.util.Set;
import java.util.function.Function;
@Data @Data
public class AlarmRuleState { class AlarmRuleState {
private final AlarmSeverity severity; private final AlarmSeverity severity;
private final AlarmRule alarmRule; private final AlarmRule alarmRule;
private final AlarmConditionSpec spec; private final AlarmConditionSpec spec;
private final long requiredDurationInMs; private final long requiredDurationInMs;
private final long requiredRepeats; private final long requiredRepeats;
private final Set<EntityKey> entityKeys;
private PersistedAlarmRuleState state; private PersistedAlarmRuleState state;
private boolean updateFlag; private boolean updateFlag;
public AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, PersistedAlarmRuleState state) { AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, Set<EntityKey> entityKeys, PersistedAlarmRuleState state) {
this.severity = severity; this.severity = severity;
this.alarmRule = alarmRule; this.alarmRule = alarmRule;
this.entityKeys = entityKeys;
if (state != null) { if (state != null) {
this.state = state; this.state = state;
} else { } else {
@ -76,6 +82,30 @@ public class AlarmRuleState {
this.requiredRepeats = requiredRepeats; this.requiredRepeats = requiredRepeats;
} }
public boolean validateTsUpdate(Set<EntityKey> changedKeys) {
for (EntityKey key : changedKeys) {
if (entityKeys.contains(key)) {
return true;
}
}
return false;
}
public boolean validateAttrUpdate(Set<EntityKey> changedKeys) {
//If the attribute was updated, but no new telemetry arrived - we ignore this until new telemetry is there.
for (EntityKey key : entityKeys) {
if (key.getType().equals(EntityKeyType.TIME_SERIES)) {
return false;
}
}
for (EntityKey key : changedKeys) {
if (entityKeys.contains(key)) {
return true;
}
}
return false;
}
public AlarmConditionSpec getSpec(AlarmRule alarmRule) { public AlarmConditionSpec getSpec(AlarmRule alarmRule) {
AlarmConditionSpec spec = alarmRule.getCondition().getSpec(); AlarmConditionSpec spec = alarmRule.getCondition().getSpec();
if (spec == null) { if (spec == null) {
@ -93,7 +123,7 @@ public class AlarmRuleState {
} }
} }
public boolean eval(DeviceDataSnapshot data) { public boolean eval(DataSnapshot data) {
boolean active = isActive(data.getTs()); boolean active = isActive(data.getTs());
switch (spec.getType()) { switch (spec.getType()) {
case SIMPLE: case SIMPLE:
@ -135,9 +165,7 @@ public class AlarmRuleState {
return false; return false;
} }
} }
long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli(); return isActive(eventTs, zoneId, zdt, schedule.getStartsOn(), schedule.getEndsOn());
long msFromStartOfDay = eventTs - startOfDay;
return schedule.getStartsOn() <= msFromStartOfDay && schedule.getEndsOn() > msFromStartOfDay;
} }
private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) { private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) {
@ -147,9 +175,7 @@ public class AlarmRuleState {
for (CustomTimeScheduleItem item : schedule.getItems()) { for (CustomTimeScheduleItem item : schedule.getItems()) {
if (item.getDayOfWeek() == dayOfWeek) { if (item.getDayOfWeek() == dayOfWeek) {
if (item.isEnabled()) { if (item.isEnabled()) {
long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli(); return isActive(eventTs, zoneId, zdt, item.getStartsOn(), item.getEndsOn());
long msFromStartOfDay = eventTs - startOfDay;
return item.getStartsOn() <= msFromStartOfDay && item.getEndsOn() > msFromStartOfDay;
} else { } else {
return false; return false;
} }
@ -158,6 +184,16 @@ public class AlarmRuleState {
return false; return false;
} }
private boolean isActive(long eventTs, ZoneId zoneId, ZonedDateTime zdt, long startsOn, long endsOn) {
long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli();
long msFromStartOfDay = eventTs - startOfDay;
if (startsOn <= endsOn) {
return startsOn <= msFromStartOfDay && endsOn > msFromStartOfDay;
} else {
return startsOn < msFromStartOfDay || (0 < msFromStartOfDay && msFromStartOfDay < endsOn);
}
}
public void clear() { public void clear() {
if (state.getEventCount() > 0 || state.getLastEventTs() > 0 || state.getDuration() > 0) { if (state.getEventCount() > 0 || state.getLastEventTs() > 0 || state.getDuration() > 0) {
state.setEventCount(0L); state.setEventCount(0L);
@ -167,7 +203,7 @@ public class AlarmRuleState {
} }
} }
private boolean evalRepeating(DeviceDataSnapshot data, boolean active) { private boolean evalRepeating(DataSnapshot data, boolean active) {
if (active && eval(alarmRule.getCondition(), data)) { if (active && eval(alarmRule.getCondition(), data)) {
state.setEventCount(state.getEventCount() + 1); state.setEventCount(state.getEventCount() + 1);
updateFlag = true; updateFlag = true;
@ -177,7 +213,7 @@ public class AlarmRuleState {
} }
} }
private boolean evalDuration(DeviceDataSnapshot data, boolean active) { private boolean evalDuration(DataSnapshot data, boolean active) {
if (active && eval(alarmRule.getCondition(), data)) { if (active && eval(alarmRule.getCondition(), data)) {
if (state.getLastEventTs() > 0) { if (state.getLastEventTs() > 0) {
if (data.getTs() > state.getLastEventTs()) { if (data.getTs() > state.getLastEventTs()) {
@ -211,45 +247,45 @@ public class AlarmRuleState {
} }
} }
private boolean eval(AlarmCondition condition, DeviceDataSnapshot data) { private boolean eval(AlarmCondition condition, DataSnapshot data) {
boolean eval = true; boolean eval = true;
for (KeyFilter keyFilter : condition.getCondition()) { for (KeyFilter keyFilter : condition.getCondition()) {
EntityKeyValue value = data.getValue(keyFilter.getKey()); EntityKeyValue value = data.getValue(keyFilter.getKey());
if (value == null) { if (value == null) {
return false; return false;
} }
eval = eval && eval(value, keyFilter.getPredicate()); eval = eval && eval(data, value, keyFilter.getPredicate());
} }
return eval; return eval;
} }
private boolean eval(EntityKeyValue value, KeyFilterPredicate predicate) { private boolean eval(DataSnapshot data, EntityKeyValue value, KeyFilterPredicate predicate) {
switch (predicate.getType()) { switch (predicate.getType()) {
case STRING: case STRING:
return evalStrPredicate(value, (StringFilterPredicate) predicate); return evalStrPredicate(data, value, (StringFilterPredicate) predicate);
case NUMERIC: case NUMERIC:
return evalNumPredicate(value, (NumericFilterPredicate) predicate); return evalNumPredicate(data, value, (NumericFilterPredicate) predicate);
case COMPLEX:
return evalComplexPredicate(value, (ComplexFilterPredicate) predicate);
case BOOLEAN: case BOOLEAN:
return evalBoolPredicate(value, (BooleanFilterPredicate) predicate); return evalBoolPredicate(data, value, (BooleanFilterPredicate) predicate);
case COMPLEX:
return evalComplexPredicate(data, value, (ComplexFilterPredicate) predicate);
default: default:
return false; return false;
} }
} }
private boolean evalComplexPredicate(EntityKeyValue ekv, ComplexFilterPredicate predicate) { private boolean evalComplexPredicate(DataSnapshot data, EntityKeyValue ekv, ComplexFilterPredicate predicate) {
switch (predicate.getOperation()) { switch (predicate.getOperation()) {
case OR: case OR:
for (KeyFilterPredicate kfp : predicate.getPredicates()) { for (KeyFilterPredicate kfp : predicate.getPredicates()) {
if (eval(ekv, kfp)) { if (eval(data, ekv, kfp)) {
return true; return true;
} }
} }
return false; return false;
case AND: case AND:
for (KeyFilterPredicate kfp : predicate.getPredicates()) { for (KeyFilterPredicate kfp : predicate.getPredicates()) {
if (!eval(ekv, kfp)) { if (!eval(data, ekv, kfp)) {
return false; return false;
} }
} }
@ -259,109 +295,55 @@ public class AlarmRuleState {
} }
} }
private boolean evalBoolPredicate(EntityKeyValue ekv, BooleanFilterPredicate predicate) { private boolean evalBoolPredicate(DataSnapshot data, EntityKeyValue ekv, BooleanFilterPredicate predicate) {
Boolean value; Boolean val = getBoolValue(ekv);
switch (ekv.getDataType()) { if (val == null) {
case LONG:
value = ekv.getLngValue() > 0;
break;
case DOUBLE:
value = ekv.getDblValue() > 0;
break;
case BOOLEAN:
value = ekv.getBoolValue();
break;
case STRING:
try {
value = Boolean.parseBoolean(ekv.getStrValue());
break;
} catch (RuntimeException e) {
return false;
}
case JSON:
try {
value = Boolean.parseBoolean(ekv.getJsonValue());
break;
} catch (RuntimeException e) {
return false;
}
default:
return false;
}
if (value == null) {
return false; return false;
} }
Boolean predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getBoolValue);
switch (predicate.getOperation()) { switch (predicate.getOperation()) {
case EQUAL: case EQUAL:
return value.equals(predicate.getValue().getDefaultValue()); return val.equals(predicateValue);
case NOT_EQUAL: case NOT_EQUAL:
return !value.equals(predicate.getValue().getDefaultValue()); return !val.equals(predicateValue);
default: default:
throw new RuntimeException("Operation not supported: " + predicate.getOperation()); throw new RuntimeException("Operation not supported: " + predicate.getOperation());
} }
} }
private boolean evalNumPredicate(EntityKeyValue ekv, NumericFilterPredicate predicate) { private boolean evalNumPredicate(DataSnapshot data, EntityKeyValue ekv, NumericFilterPredicate predicate) {
Double value; Double val = getDblValue(ekv);
switch (ekv.getDataType()) { if (val == null) {
case LONG:
value = ekv.getLngValue().doubleValue();
break;
case DOUBLE:
value = ekv.getDblValue();
break;
case BOOLEAN:
value = ekv.getBoolValue() ? 1.0 : 0.0;
break;
case STRING:
try {
value = Double.parseDouble(ekv.getStrValue());
break;
} catch (RuntimeException e) {
return false;
}
case JSON:
try {
value = Double.parseDouble(ekv.getJsonValue());
break;
} catch (RuntimeException e) {
return false;
}
default:
return false;
}
if (value == null) {
return false; return false;
} }
Double predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getDblValue);
Double predicateValue = predicate.getValue().getDefaultValue();
switch (predicate.getOperation()) { switch (predicate.getOperation()) {
case NOT_EQUAL: case NOT_EQUAL:
return !value.equals(predicateValue); return !val.equals(predicateValue);
case EQUAL: case EQUAL:
return value.equals(predicateValue); return val.equals(predicateValue);
case GREATER: case GREATER:
return value > predicateValue; return val > predicateValue;
case GREATER_OR_EQUAL: case GREATER_OR_EQUAL:
return value >= predicateValue; return val >= predicateValue;
case LESS: case LESS:
return value < predicateValue; return val < predicateValue;
case LESS_OR_EQUAL: case LESS_OR_EQUAL:
return value <= predicateValue; return val <= predicateValue;
default: default:
throw new RuntimeException("Operation not supported: " + predicate.getOperation()); throw new RuntimeException("Operation not supported: " + predicate.getOperation());
} }
} }
private boolean evalStrPredicate(EntityKeyValue ekv, StringFilterPredicate predicate) { private boolean evalStrPredicate(DataSnapshot data, EntityKeyValue ekv, StringFilterPredicate predicate) {
String val; String val = getStrValue(ekv);
String predicateValue; if (val == null) {
return false;
}
String predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getStrValue);
if (predicate.isIgnoreCase()) { if (predicate.isIgnoreCase()) {
val = ekv.getStrValue().toLowerCase(); val = val.toLowerCase();
predicateValue = predicate.getValue().getDefaultValue().toLowerCase(); predicateValue = predicateValue.toLowerCase();
} else {
val = ekv.getStrValue();
predicateValue = predicate.getValue().getDefaultValue();
} }
switch (predicate.getOperation()) { switch (predicate.getOperation()) {
case CONTAINS: case CONTAINS:
@ -380,4 +362,100 @@ public class AlarmRuleState {
throw new RuntimeException("Operation not supported: " + predicate.getOperation()); throw new RuntimeException("Operation not supported: " + predicate.getOperation());
} }
} }
private <T> T getPredicateValue(DataSnapshot data, FilterPredicateValue<T> value, Function<EntityKeyValue, T> transformFunction) {
EntityKeyValue ekv = getDynamicPredicateValue(data, value);
if (ekv != null) {
T result = transformFunction.apply(ekv);
if (result != null) {
return result;
}
}
return value.getDefaultValue();
}
private <T> EntityKeyValue getDynamicPredicateValue(DataSnapshot data, FilterPredicateValue<T> value) {
EntityKeyValue ekv = null;
if (value.getDynamicValue() != null) {
ekv = data.getValue(new EntityKey(EntityKeyType.ATTRIBUTE, value.getDynamicValue().getSourceAttribute()));
if (ekv == null) {
ekv = data.getValue(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, value.getDynamicValue().getSourceAttribute()));
if (ekv == null) {
ekv = data.getValue(new EntityKey(EntityKeyType.SHARED_ATTRIBUTE, value.getDynamicValue().getSourceAttribute()));
if (ekv == null) {
ekv = data.getValue(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, value.getDynamicValue().getSourceAttribute()));
}
}
}
}
return ekv;
}
private static String getStrValue(EntityKeyValue ekv) {
switch (ekv.getDataType()) {
case LONG:
return ekv.getLngValue() != null ? ekv.getLngValue().toString() : null;
case DOUBLE:
return ekv.getDblValue() != null ? ekv.getDblValue().toString() : null;
case BOOLEAN:
return ekv.getBoolValue() != null ? ekv.getBoolValue().toString() : null;
case STRING:
return ekv.getStrValue();
case JSON:
return ekv.getJsonValue();
default:
return null;
}
}
private static Double getDblValue(EntityKeyValue ekv) {
switch (ekv.getDataType()) {
case LONG:
return ekv.getLngValue() != null ? ekv.getLngValue().doubleValue() : null;
case DOUBLE:
return ekv.getDblValue() != null ? ekv.getDblValue() : null;
case BOOLEAN:
return ekv.getBoolValue() != null ? (ekv.getBoolValue() ? 1.0 : 0.0) : null;
case STRING:
try {
return Double.parseDouble(ekv.getStrValue());
} catch (RuntimeException e) {
return null;
}
case JSON:
try {
return Double.parseDouble(ekv.getJsonValue());
} catch (RuntimeException e) {
return null;
}
default:
return null;
}
}
private static Boolean getBoolValue(EntityKeyValue ekv) {
switch (ekv.getDataType()) {
case LONG:
return ekv.getLngValue() != null ? ekv.getLngValue() > 0 : null;
case DOUBLE:
return ekv.getDblValue() != null ? ekv.getDblValue() > 0 : null;
case BOOLEAN:
return ekv.getBoolValue();
case STRING:
try {
return Boolean.parseBoolean(ekv.getStrValue());
} catch (RuntimeException e) {
return null;
}
case JSON:
try {
return Boolean.parseBoolean(ekv.getJsonValue());
} catch (RuntimeException e) {
return null;
}
default:
return null;
}
}
} }

49
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileAlarmState.java → rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java

@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.profile;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.action.TbAlarmResult; import org.thingsboard.rule.engine.action.TbAlarmResult;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.profile.state.PersistedAlarmRuleState; import org.thingsboard.rule.engine.profile.state.PersistedAlarmRuleState;
@ -27,6 +28,7 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.queue.ServiceQueue;
@ -39,8 +41,10 @@ import java.util.concurrent.ExecutionException;
import java.util.function.BiFunction; import java.util.function.BiFunction;
@Data @Data
class DeviceProfileAlarmState { @Slf4j
class AlarmState {
private final ProfileState deviceProfile;
private final EntityId originator; private final EntityId originator;
private DeviceProfileAlarm alarmDefinition; private DeviceProfileAlarm alarmDefinition;
private volatile List<AlarmRuleState> createRulesSortedBySeverityDesc; private volatile List<AlarmRuleState> createRulesSortedBySeverityDesc;
@ -50,27 +54,33 @@ class DeviceProfileAlarmState {
private volatile TbMsgMetaData lastMsgMetaData; private volatile TbMsgMetaData lastMsgMetaData;
private volatile String lastMsgQueueName; private volatile String lastMsgQueueName;
public DeviceProfileAlarmState(EntityId originator, DeviceProfileAlarm alarmDefinition, PersistedAlarmState alarmState) { AlarmState(ProfileState deviceProfile, EntityId originator, DeviceProfileAlarm alarmDefinition, PersistedAlarmState alarmState) {
this.deviceProfile = deviceProfile;
this.originator = originator; this.originator = originator;
this.updateState(alarmDefinition, alarmState); this.updateState(alarmDefinition, alarmState);
} }
public boolean process(TbContext ctx, TbMsg msg, DeviceDataSnapshot data) throws ExecutionException, InterruptedException { public boolean process(TbContext ctx, TbMsg msg, DataSnapshot data, SnapshotUpdate update) throws ExecutionException, InterruptedException {
initCurrentAlarm(ctx); initCurrentAlarm(ctx);
lastMsgMetaData = msg.getMetaData(); lastMsgMetaData = msg.getMetaData();
lastMsgQueueName = msg.getQueueName(); lastMsgQueueName = msg.getQueueName();
return createOrClearAlarms(ctx, data, AlarmRuleState::eval); return createOrClearAlarms(ctx, data, update, AlarmRuleState::eval);
} }
public boolean process(TbContext ctx, long ts) throws ExecutionException, InterruptedException { public boolean process(TbContext ctx, long ts) throws ExecutionException, InterruptedException {
initCurrentAlarm(ctx); initCurrentAlarm(ctx);
return createOrClearAlarms(ctx, ts, AlarmRuleState::eval); return createOrClearAlarms(ctx, ts, null, AlarmRuleState::eval);
} }
public <T> boolean createOrClearAlarms(TbContext ctx, T data, BiFunction<AlarmRuleState, T, Boolean> evalFunction) { public <T> boolean createOrClearAlarms(TbContext ctx, T data, SnapshotUpdate update, BiFunction<AlarmRuleState, T, Boolean> evalFunction) {
boolean stateUpdate = false; boolean stateUpdate = false;
AlarmSeverity resultSeverity = null; AlarmSeverity resultSeverity = null;
log.debug("[{}] processing update: {}", alarmDefinition.getId(), data);
for (AlarmRuleState state : createRulesSortedBySeverityDesc) { for (AlarmRuleState state : createRulesSortedBySeverityDesc) {
if (!validateUpdate(update, state)) {
log.debug("[{}][{}] Update is not valid for current rule state", alarmDefinition.getId(), state.getSeverity());
continue;
}
boolean evalResult = evalFunction.apply(state, data); boolean evalResult = evalFunction.apply(state, data);
stateUpdate |= state.checkUpdate(); stateUpdate |= state.checkUpdate();
if (evalResult) { if (evalResult) {
@ -81,9 +91,17 @@ class DeviceProfileAlarmState {
if (resultSeverity != null) { if (resultSeverity != null) {
pushMsg(ctx, calculateAlarmResult(ctx, resultSeverity)); pushMsg(ctx, calculateAlarmResult(ctx, resultSeverity));
} else if (currentAlarm != null && clearState != null) { } else if (currentAlarm != null && clearState != null) {
if (!validateUpdate(update, clearState)) {
log.debug("[{}] Update is not valid for current clear state", alarmDefinition.getId());
return stateUpdate;
}
Boolean evalResult = evalFunction.apply(clearState, data); Boolean evalResult = evalFunction.apply(clearState, data);
if (evalResult) { if (evalResult) {
stateUpdate |= clearState.checkUpdate(); stateUpdate |= clearState.checkUpdate();
for (AlarmRuleState state : createRulesSortedBySeverityDesc) {
state.clear();
stateUpdate |= state.checkUpdate();
}
ctx.getAlarmService().clearAlarm(ctx.getTenantId(), currentAlarm.getId(), JacksonUtil.OBJECT_MAPPER.createObjectNode(), System.currentTimeMillis()); ctx.getAlarmService().clearAlarm(ctx.getTenantId(), currentAlarm.getId(), JacksonUtil.OBJECT_MAPPER.createObjectNode(), System.currentTimeMillis());
pushMsg(ctx, new TbAlarmResult(false, false, true, currentAlarm)); pushMsg(ctx, new TbAlarmResult(false, false, true, currentAlarm));
currentAlarm = null; currentAlarm = null;
@ -92,6 +110,18 @@ class DeviceProfileAlarmState {
return stateUpdate; return stateUpdate;
} }
public boolean validateUpdate(SnapshotUpdate update, AlarmRuleState state) {
if (update != null) {
//Check that the update type and that keys match.
if (update.getType().equals(EntityKeyType.TIME_SERIES)) {
return state.validateTsUpdate(update.getKeys());
} else if (update.getType().equals(EntityKeyType.ATTRIBUTE)) {
return state.validateAttrUpdate(update.getKeys());
}
}
return true;
}
public void initCurrentAlarm(TbContext ctx) throws InterruptedException, ExecutionException { public void initCurrentAlarm(TbContext ctx) throws InterruptedException, ExecutionException {
if (!initialFetchDone) { if (!initialFetchDone) {
Alarm alarm = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), originator, alarmDefinition.getAlarmType()).get(); Alarm alarm = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), originator, alarmDefinition.getAlarmType()).get();
@ -137,17 +167,20 @@ class DeviceProfileAlarmState {
alarmState.getCreateRuleStates().put(severity, ruleState); alarmState.getCreateRuleStates().put(severity, ruleState);
} }
} }
createRulesSortedBySeverityDesc.add(new AlarmRuleState(severity, rule, ruleState)); createRulesSortedBySeverityDesc.add(new AlarmRuleState(severity, rule,
deviceProfile.getCreateAlarmKeys(alarm.getId(), severity), ruleState));
}); });
createRulesSortedBySeverityDesc.sort(Comparator.comparingInt(state -> state.getSeverity().ordinal())); createRulesSortedBySeverityDesc.sort(Comparator.comparingInt(state -> state.getSeverity().ordinal()));
PersistedAlarmRuleState ruleState = alarmState == null ? null : alarmState.getClearRuleState(); PersistedAlarmRuleState ruleState = alarmState == null ? null : alarmState.getClearRuleState();
if (alarmDefinition.getClearRule() != null) { if (alarmDefinition.getClearRule() != null) {
clearState = new AlarmRuleState(null, alarmDefinition.getClearRule(), ruleState); clearState = new AlarmRuleState(null, alarmDefinition.getClearRule(), deviceProfile.getClearAlarmKeys(alarm.getId()), ruleState);
} }
} }
private TbAlarmResult calculateAlarmResult(TbContext ctx, AlarmSeverity severity) { private TbAlarmResult calculateAlarmResult(TbContext ctx, AlarmSeverity severity) {
if (currentAlarm != null) { if (currentAlarm != null) {
// 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).
// Maybe we should fetch alarm every time?
currentAlarm.setEndTs(System.currentTimeMillis()); currentAlarm.setEndTs(System.currentTimeMillis());
AlarmSeverity oldSeverity = currentAlarm.getSeverity(); AlarmSeverity oldSeverity = currentAlarm.getSeverity();
if (!oldSeverity.equals(severity)) { if (!oldSeverity.equals(severity)) {

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java

@ -15,7 +15,7 @@
*/ */
package org.thingsboard.rule.engine.profile; package org.thingsboard.rule.engine.profile;
public enum AlarmStateUpdateResult { enum AlarmStateUpdateResult {
NONE, CREATED, UPDATED, SEVERITY_UPDATED, CLEARED; NONE, CREATED, UPDATED, SEVERITY_UPDATED, CLEARED;

34
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceDataSnapshot.java → rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java

@ -24,7 +24,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public class DeviceDataSnapshot { class DataSnapshot {
private volatile boolean ready; private volatile boolean ready;
@Getter @Getter
@ -33,7 +33,7 @@ public class DeviceDataSnapshot {
private final Set<EntityKey> keys; private final Set<EntityKey> keys;
private final Map<EntityKey, EntityKeyValue> values = new ConcurrentHashMap<>(); private final Map<EntityKey, EntityKeyValue> values = new ConcurrentHashMap<>();
public DeviceDataSnapshot(Set<EntityKey> entityKeysToFetch) { DataSnapshot(Set<EntityKey> entityKeysToFetch) {
this.keys = entityKeysToFetch; this.keys = entityKeysToFetch;
} }
@ -56,28 +56,38 @@ public class DeviceDataSnapshot {
} }
} }
void putValue(EntityKey key, EntityKeyValue value) { boolean putValue(EntityKey key, long newTs, EntityKeyValue value) {
boolean updateOfTs = ts != newTs;
boolean result = false;
switch (key.getType()) { switch (key.getType()) {
case ATTRIBUTE: case ATTRIBUTE:
putIfKeyExists(key, value); result |= putIfKeyExists(key, value, updateOfTs);
putIfKeyExists(getAttrKey(key, EntityKeyType.CLIENT_ATTRIBUTE), value); result |= putIfKeyExists(getAttrKey(key, EntityKeyType.CLIENT_ATTRIBUTE), value, updateOfTs);
putIfKeyExists(getAttrKey(key, EntityKeyType.SHARED_ATTRIBUTE), value); result |= putIfKeyExists(getAttrKey(key, EntityKeyType.SHARED_ATTRIBUTE), value, updateOfTs);
putIfKeyExists(getAttrKey(key, EntityKeyType.SERVER_ATTRIBUTE), value); result |= putIfKeyExists(getAttrKey(key, EntityKeyType.SERVER_ATTRIBUTE), value, updateOfTs);
break; break;
case CLIENT_ATTRIBUTE: case CLIENT_ATTRIBUTE:
case SHARED_ATTRIBUTE: case SHARED_ATTRIBUTE:
case SERVER_ATTRIBUTE: case SERVER_ATTRIBUTE:
putIfKeyExists(key, value); result |= putIfKeyExists(key, value, updateOfTs);
putIfKeyExists(getAttrKey(key, EntityKeyType.ATTRIBUTE), value); result |= putIfKeyExists(getAttrKey(key, EntityKeyType.ATTRIBUTE), value, updateOfTs);
break; break;
default: default:
putIfKeyExists(key, value); result |= putIfKeyExists(key, value, updateOfTs);
} }
return result;
} }
private void putIfKeyExists(EntityKey key, EntityKeyValue value) { private boolean putIfKeyExists(EntityKey key, EntityKeyValue value, boolean updateOfTs) {
if (keys.contains(key)) { if (keys.contains(key)) {
values.put(key, value); EntityKeyValue oldValue = values.put(key, value);
if (updateOfTs) {
return true;
} else {
return oldValue == null || !oldValue.equals(value);
}
} else {
return false;
} }
} }

63
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileState.java

@ -1,63 +0,0 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.profile;
import lombok.AccessLevel;
import lombok.Getter;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.profile.AlarmRule;
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.KeyFilter;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
class DeviceProfileState {
private DeviceProfile deviceProfile;
@Getter(AccessLevel.PACKAGE)
private final List<DeviceProfileAlarm> alarmSettings = new CopyOnWriteArrayList<>();
@Getter(AccessLevel.PACKAGE)
private final Set<EntityKey> entityKeys = ConcurrentHashMap.newKeySet();
DeviceProfileState(DeviceProfile deviceProfile) {
updateDeviceProfile(deviceProfile);
}
void updateDeviceProfile(DeviceProfile deviceProfile) {
this.deviceProfile = deviceProfile;
alarmSettings.clear();
if (deviceProfile.getProfileData().getAlarms() != null) {
alarmSettings.addAll(deviceProfile.getProfileData().getAlarms());
for (DeviceProfileAlarm alarm : deviceProfile.getProfileData().getAlarms()) {
for (AlarmRule alarmRule : alarm.getCreateRules().values()) {
for (KeyFilter keyFilter : alarmRule.getCondition().getCondition()) {
entityKeys.add(keyFilter.getKey());
}
}
}
}
}
public DeviceProfileId getProfileId() {
return deviceProfile.getId();
}
}

104
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java

@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.profile; package org.thingsboard.rule.engine.profile;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.profile.state.PersistedAlarmState; import org.thingsboard.rule.engine.profile.state.PersistedAlarmState;
@ -29,7 +30,6 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleNodeStateId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
@ -53,17 +53,18 @@ import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j
class DeviceState { class DeviceState {
private final boolean persistState; private final boolean persistState;
private final DeviceId deviceId; private final DeviceId deviceId;
private final ProfileState deviceProfile;
private RuleNodeState state; private RuleNodeState state;
private DeviceProfileState deviceProfile;
private PersistedDeviceState pds; private PersistedDeviceState pds;
private DeviceDataSnapshot latestValues; private DataSnapshot latestValues;
private final ConcurrentMap<String, DeviceProfileAlarmState> alarmStates = new ConcurrentHashMap<>(); private final ConcurrentMap<String, AlarmState> alarmStates = new ConcurrentHashMap<>();
public DeviceState(TbContext ctx, TbDeviceProfileNodeConfiguration config, DeviceId deviceId, DeviceProfileState deviceProfile, RuleNodeState state) { DeviceState(TbContext ctx, TbDeviceProfileNodeConfiguration config, DeviceId deviceId, ProfileState deviceProfile, RuleNodeState state) {
this.persistState = config.isPersistAlarmRulesState(); this.persistState = config.isPersistAlarmRulesState();
this.deviceId = deviceId; this.deviceId = deviceId;
this.deviceProfile = deviceProfile; this.deviceProfile = deviceProfile;
@ -86,7 +87,7 @@ class DeviceState {
if (pds != null) { if (pds != null) {
for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) {
alarmStates.computeIfAbsent(alarm.getId(), alarmStates.computeIfAbsent(alarm.getId(),
a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); a -> new AlarmState(deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm)));
} }
} }
} }
@ -107,14 +108,20 @@ class DeviceState {
if (alarmStates.containsKey(alarm.getId())) { if (alarmStates.containsKey(alarm.getId())) {
alarmStates.get(alarm.getId()).updateState(alarm, getOrInitPersistedAlarmState(alarm)); alarmStates.get(alarm.getId()).updateState(alarm, getOrInitPersistedAlarmState(alarm));
} else { } else {
alarmStates.putIfAbsent(alarm.getId(), new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); alarmStates.putIfAbsent(alarm.getId(), new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm)));
} }
} }
} }
public void harvestAlarms(TbContext ctx, long ts) throws ExecutionException, InterruptedException { public void harvestAlarms(TbContext ctx, long ts) throws ExecutionException, InterruptedException {
for (DeviceProfileAlarmState state : alarmStates.values()) { log.debug("[{}] Going to harvest alarms: {}", ctx.getSelfId(), ts);
state.process(ctx, ts); boolean stateChanged = false;
for (AlarmState state : alarmStates.values()) {
stateChanged |= state.process(ctx, ts);
}
if (persistState && stateChanged) {
state.setStateData(JacksonUtil.toString(pds));
state = ctx.saveRuleNodeState(state);
} }
} }
@ -146,8 +153,8 @@ class DeviceState {
boolean stateChanged = false; boolean stateChanged = false;
Alarm alarmNf = JacksonUtil.fromString(msg.getData(), Alarm.class); Alarm alarmNf = JacksonUtil.fromString(msg.getData(), Alarm.class);
for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) {
DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(),
a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm)));
stateChanged |= alarmState.processAlarmClear(ctx, alarmNf); stateChanged |= alarmState.processAlarmClear(ctx, alarmNf);
} }
ctx.tellSuccess(msg); ctx.tellSuccess(msg);
@ -175,9 +182,9 @@ class DeviceState {
EntityKeyType keyType = getKeyTypeFromScope(scope); EntityKeyType keyType = getKeyTypeFromScope(scope);
keys.forEach(key -> latestValues.removeValue(new EntityKey(keyType, key))); keys.forEach(key -> latestValues.removeValue(new EntityKey(keyType, key)));
for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) {
DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(),
a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm)));
stateChanged |= alarmState.process(ctx, msg, latestValues); stateChanged |= alarmState.process(ctx, msg, latestValues, null);
} }
} }
ctx.tellSuccess(msg); ctx.tellSuccess(msg);
@ -192,11 +199,11 @@ class DeviceState {
private boolean processAttributesUpdate(TbContext ctx, TbMsg msg, Set<AttributeKvEntry> attributes, String scope) throws ExecutionException, InterruptedException { private boolean processAttributesUpdate(TbContext ctx, TbMsg msg, Set<AttributeKvEntry> attributes, String scope) throws ExecutionException, InterruptedException {
boolean stateChanged = false; boolean stateChanged = false;
if (!attributes.isEmpty()) { if (!attributes.isEmpty()) {
merge(latestValues, attributes, scope); SnapshotUpdate update = merge(latestValues, attributes, scope);
for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) {
DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(),
a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm)));
stateChanged |= alarmState.process(ctx, msg, latestValues); stateChanged |= alarmState.process(ctx, msg, latestValues, update);
} }
} }
ctx.tellSuccess(msg); ctx.tellSuccess(msg);
@ -206,34 +213,47 @@ class DeviceState {
protected boolean processTelemetry(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { protected boolean processTelemetry(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException {
boolean stateChanged = false; boolean stateChanged = false;
Map<Long, List<KvEntry>> tsKvMap = JsonConverter.convertToSortedTelemetry(new JsonParser().parse(msg.getData()), TbMsgTimeseriesNode.getTs(msg)); Map<Long, List<KvEntry>> tsKvMap = JsonConverter.convertToSortedTelemetry(new JsonParser().parse(msg.getData()), TbMsgTimeseriesNode.getTs(msg));
// iterate over data by ts (ASC order).
for (Map.Entry<Long, List<KvEntry>> entry : tsKvMap.entrySet()) { for (Map.Entry<Long, List<KvEntry>> entry : tsKvMap.entrySet()) {
Long ts = entry.getKey(); Long ts = entry.getKey();
List<KvEntry> data = entry.getValue(); List<KvEntry> data = entry.getValue();
merge(latestValues, ts, data); SnapshotUpdate update = merge(latestValues, ts, data);
for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { if (update.hasUpdate()) {
DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) {
a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(),
stateChanged |= alarmState.process(ctx, msg, latestValues); a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm)));
stateChanged |= alarmState.process(ctx, msg, latestValues, update);
}
} }
} }
ctx.tellSuccess(msg); ctx.tellSuccess(msg);
return stateChanged; return stateChanged;
} }
private void merge(DeviceDataSnapshot latestValues, Long ts, List<KvEntry> data) { private SnapshotUpdate merge(DataSnapshot latestValues, Long newTs, List<KvEntry> data) {
latestValues.setTs(ts); Set<EntityKey> keys = new HashSet<>();
for (KvEntry entry : data) { for (KvEntry entry : data) {
latestValues.putValue(new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()), toEntityValue(entry)); EntityKey entityKey = new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey());
if (latestValues.putValue(entityKey, newTs, toEntityValue(entry))) {
keys.add(entityKey);
}
} }
latestValues.setTs(newTs);
return new SnapshotUpdate(EntityKeyType.TIME_SERIES, keys);
} }
private void merge(DeviceDataSnapshot latestValues, Set<AttributeKvEntry> attributes, String scope) { private SnapshotUpdate merge(DataSnapshot latestValues, Set<AttributeKvEntry> attributes, String scope) {
long ts = latestValues.getTs(); long newTs = 0;
Set<EntityKey> keys = new HashSet<>();
for (AttributeKvEntry entry : attributes) { for (AttributeKvEntry entry : attributes) {
ts = Math.max(ts, entry.getLastUpdateTs()); newTs = Math.max(newTs, entry.getLastUpdateTs());
latestValues.putValue(new EntityKey(getKeyTypeFromScope(scope), entry.getKey()), toEntityValue(entry)); EntityKey entityKey = new EntityKey(getKeyTypeFromScope(scope), entry.getKey());
if (latestValues.putValue(entityKey, newTs, toEntityValue(entry))) {
keys.add(entityKey);
}
} }
latestValues.setTs(ts); latestValues.setTs(newTs);
return new SnapshotUpdate(EntityKeyType.ATTRIBUTE, keys);
} }
private static EntityKeyType getKeyTypeFromScope(String scope) { private static EntityKeyType getKeyTypeFromScope(String scope) {
@ -248,14 +268,14 @@ class DeviceState {
return EntityKeyType.ATTRIBUTE; return EntityKeyType.ATTRIBUTE;
} }
private DeviceDataSnapshot fetchLatestValues(TbContext ctx, EntityId originator) throws ExecutionException, InterruptedException { private DataSnapshot fetchLatestValues(TbContext ctx, EntityId originator) throws ExecutionException, InterruptedException {
Set<EntityKey> entityKeysToFetch = deviceProfile.getEntityKeys(); Set<EntityKey> entityKeysToFetch = deviceProfile.getEntityKeys();
DeviceDataSnapshot result = new DeviceDataSnapshot(entityKeysToFetch); DataSnapshot result = new DataSnapshot(entityKeysToFetch);
addEntityKeysToSnapshot(ctx, originator, entityKeysToFetch, result); addEntityKeysToSnapshot(ctx, originator, entityKeysToFetch, result);
return result; return result;
} }
private void addEntityKeysToSnapshot(TbContext ctx, EntityId originator, Set<EntityKey> entityKeysToFetch, DeviceDataSnapshot result) throws InterruptedException, ExecutionException { private void addEntityKeysToSnapshot(TbContext ctx, EntityId originator, Set<EntityKey> entityKeysToFetch, DataSnapshot result) throws InterruptedException, ExecutionException {
Set<String> serverAttributeKeys = new HashSet<>(); Set<String> serverAttributeKeys = new HashSet<>();
Set<String> clientAttributeKeys = new HashSet<>(); Set<String> clientAttributeKeys = new HashSet<>();
Set<String> sharedAttributeKeys = new HashSet<>(); Set<String> sharedAttributeKeys = new HashSet<>();
@ -291,16 +311,16 @@ class DeviceState {
if (device != null) { if (device != null) {
switch (key) { switch (key) {
case EntityKeyMapping.NAME: case EntityKeyMapping.NAME:
result.putValue(entityKey, EntityKeyValue.fromString(device.getName())); result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromString(device.getName()));
break; break;
case EntityKeyMapping.TYPE: case EntityKeyMapping.TYPE:
result.putValue(entityKey, EntityKeyValue.fromString(device.getType())); result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromString(device.getType()));
break; break;
case EntityKeyMapping.CREATED_TIME: case EntityKeyMapping.CREATED_TIME:
result.putValue(entityKey, EntityKeyValue.fromLong(device.getCreatedTime())); result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromLong(device.getCreatedTime()));
break; break;
case EntityKeyMapping.LABEL: case EntityKeyMapping.LABEL:
result.putValue(entityKey, EntityKeyValue.fromString(device.getLabel())); result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromString(device.getLabel()));
break; break;
} }
} }
@ -312,7 +332,7 @@ class DeviceState {
List<TsKvEntry> data = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), originator, latestTsKeys).get(); List<TsKvEntry> data = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), originator, latestTsKeys).get();
for (TsKvEntry entry : data) { for (TsKvEntry entry : data) {
if (entry.getValue() != null) { if (entry.getValue() != null) {
result.putValue(new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()), toEntityValue(entry)); result.putValue(new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()), entry.getTs(), toEntityValue(entry));
} }
} }
} }
@ -330,13 +350,13 @@ class DeviceState {
} }
} }
private void addToSnapshot(DeviceDataSnapshot snapshot, Set<String> commonAttributeKeys, List<AttributeKvEntry> data) { private void addToSnapshot(DataSnapshot snapshot, Set<String> commonAttributeKeys, List<AttributeKvEntry> data) {
for (AttributeKvEntry entry : data) { for (AttributeKvEntry entry : data) {
if (entry.getValue() != null) { if (entry.getValue() != null) {
EntityKeyValue value = toEntityValue(entry); EntityKeyValue value = toEntityValue(entry);
snapshot.putValue(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, entry.getKey()), value); snapshot.putValue(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, entry.getKey()), entry.getLastUpdateTs(), value);
if (commonAttributeKeys.contains(entry.getKey())) { if (commonAttributeKeys.contains(entry.getKey())) {
snapshot.putValue(new EntityKey(EntityKeyType.ATTRIBUTE, entry.getKey()), value); snapshot.putValue(new EntityKey(EntityKeyType.ATTRIBUTE, entry.getKey()), entry.getLastUpdateTs(), value);
} }
} }
} }

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java

@ -15,9 +15,11 @@
*/ */
package org.thingsboard.rule.engine.profile; package org.thingsboard.rule.engine.profile;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DataType;
@EqualsAndHashCode
class EntityKeyValue { class EntityKeyValue {
@Getter @Getter

137
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java

@ -0,0 +1,137 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.profile;
import lombok.AccessLevel;
import lombok.Getter;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.device.profile.AlarmRule;
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.DynamicValueSourceType;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.KeyFilterPredicate;
import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate;
import org.thingsboard.server.common.data.query.StringFilterPredicate;
import javax.print.attribute.standard.Severity;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
class ProfileState {
private DeviceProfile deviceProfile;
@Getter(AccessLevel.PACKAGE)
private final List<DeviceProfileAlarm> alarmSettings = new CopyOnWriteArrayList<>();
@Getter(AccessLevel.PACKAGE)
private final Set<EntityKey> entityKeys = ConcurrentHashMap.newKeySet();
private final Map<String, Map<AlarmSeverity, Set<EntityKey>>> alarmCreateKeys = new HashMap<>();
private final Map<String, Set<EntityKey>> alarmClearKeys = new HashMap<>();
ProfileState(DeviceProfile deviceProfile) {
updateDeviceProfile(deviceProfile);
}
void updateDeviceProfile(DeviceProfile deviceProfile) {
this.deviceProfile = deviceProfile;
alarmSettings.clear();
alarmCreateKeys.clear();
alarmClearKeys.clear();
if (deviceProfile.getProfileData().getAlarms() != null) {
alarmSettings.addAll(deviceProfile.getProfileData().getAlarms());
for (DeviceProfileAlarm alarm : deviceProfile.getProfileData().getAlarms()) {
Map<AlarmSeverity, Set<EntityKey>> createAlarmKeys = alarmCreateKeys.computeIfAbsent(alarm.getId(), id -> new HashMap<>());
alarm.getCreateRules().forEach(((severity, alarmRule) -> {
Set<EntityKey> ruleKeys = createAlarmKeys.computeIfAbsent(severity, id -> new HashSet<>());
for (KeyFilter keyFilter : alarmRule.getCondition().getCondition()) {
entityKeys.add(keyFilter.getKey());
ruleKeys.add(keyFilter.getKey());
addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, ruleKeys);
}
}));
if (alarm.getClearRule() != null) {
Set<EntityKey> clearAlarmKeys = alarmClearKeys.computeIfAbsent(alarm.getId(), id -> new HashSet<>());
for (KeyFilter keyFilter : alarm.getClearRule().getCondition().getCondition()) {
entityKeys.add(keyFilter.getKey());
clearAlarmKeys.add(keyFilter.getKey());
addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, clearAlarmKeys);
}
}
}
}
}
private void addDynamicValuesRecursively(KeyFilterPredicate predicate, Set<EntityKey> entityKeys, Set<EntityKey> ruleKeys) {
switch (predicate.getType()) {
case STRING:
case NUMERIC:
case BOOLEAN:
DynamicValue value = ((SimpleKeyFilterPredicate) predicate).getValue().getDynamicValue();
if (value != null && value.getSourceType() == DynamicValueSourceType.CURRENT_DEVICE) {
EntityKey entityKey = new EntityKey(EntityKeyType.ATTRIBUTE, value.getSourceAttribute());
entityKeys.add(entityKey);
ruleKeys.add(entityKey);
}
break;
case COMPLEX:
for (KeyFilterPredicate child : ((ComplexFilterPredicate) predicate).getPredicates()) {
addDynamicValuesRecursively(child, entityKeys, ruleKeys);
}
break;
}
}
DeviceProfileId getProfileId() {
return deviceProfile.getId();
}
Set<EntityKey> getCreateAlarmKeys(String id, AlarmSeverity severity) {
Map<AlarmSeverity, Set<EntityKey>> sKeys = alarmCreateKeys.get(id);
if (sKeys == null) {
return Collections.emptySet();
} else {
Set<EntityKey> keys = sKeys.get(severity);
if (keys == null) {
return Collections.emptySet();
} else {
return keys;
}
}
}
Set<EntityKey> getClearAlarmKeys(String id) {
Set<EntityKey> keys = alarmClearKeys.get(id);
if (keys == null) {
return Collections.emptySet();
} else {
return keys;
}
}
}

39
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/SnapshotUpdate.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.profile;
import lombok.Getter;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import java.util.Set;
class SnapshotUpdate {
@Getter
private final EntityKeyType type;
@Getter
private final Set<EntityKey> keys;
SnapshotUpdate(EntityKeyType type, Set<EntityKey> keys) {
this.type = type;
this.keys = keys;
}
boolean hasUpdate(){
return !keys.isEmpty();
}
}

29
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java

@ -23,7 +23,6 @@ import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.profile.state.PersistedDeviceState;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
@ -36,11 +35,10 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.rule.RuleNodeState;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.dao.util.mapping.JacksonUtil; import org.thingsboard.server.dao.util.mapping.JacksonUtil;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -70,11 +68,14 @@ public class TbDeviceProfileNode implements TbNode {
this.cache = ctx.getDeviceProfileCache(); this.cache = ctx.getDeviceProfileCache();
scheduleAlarmHarvesting(ctx); scheduleAlarmHarvesting(ctx);
if (config.isFetchAlarmRulesStateOnStart()) { if (config.isFetchAlarmRulesStateOnStart()) {
log.info("[{}] Fetching alarm rule state", ctx.getSelfId());
int fetchCount = 0;
PageLink pageLink = new PageLink(1024); PageLink pageLink = new PageLink(1024);
while (true) { while (true) {
PageData<RuleNodeState> states = ctx.findRuleNodeStates(pageLink); PageData<RuleNodeState> states = ctx.findRuleNodeStates(pageLink);
if (!states.getData().isEmpty()) { if (!states.getData().isEmpty()) {
for (RuleNodeState rns : states.getData()) { for (RuleNodeState rns : states.getData()) {
fetchCount++;
if (rns.getEntityId().getEntityType().equals(EntityType.DEVICE) && ctx.isLocalEntity(rns.getEntityId())) { if (rns.getEntityId().getEntityType().equals(EntityType.DEVICE) && ctx.isLocalEntity(rns.getEntityId())) {
getOrCreateDeviceState(ctx, new DeviceId(rns.getEntityId().getId()), rns); getOrCreateDeviceState(ctx, new DeviceId(rns.getEntityId().getId()), rns);
} }
@ -86,6 +87,11 @@ public class TbDeviceProfileNode implements TbNode {
pageLink = pageLink.nextPageLink(); pageLink = pageLink.nextPageLink();
} }
} }
log.info("[{}] Fetched alarm rule state for {} entities", ctx.getSelfId(), fetchCount);
}
if (!config.isPersistAlarmRulesState() && ctx.isLocalEntity(ctx.getSelfId())) {
log.info("[{}] Going to cleanup rule node states", ctx.getSelfId());
ctx.clearRuleNodeStates();
} }
} }
@ -114,11 +120,14 @@ public class TbDeviceProfileNode implements TbNode {
} }
} }
} else if (EntityType.DEVICE_PROFILE.equals(originatorType)) { } else if (EntityType.DEVICE_PROFILE.equals(originatorType)) {
log.info("[{}] Received device profile update notification: {}", ctx.getSelfId(), msg.getData());
if (msg.getType().equals("ENTITY_UPDATED")) { if (msg.getType().equals("ENTITY_UPDATED")) {
DeviceProfile deviceProfile = JacksonUtil.fromString(msg.getData(), DeviceProfile.class); DeviceProfile deviceProfile = JacksonUtil.fromString(msg.getData(), DeviceProfile.class);
for (DeviceState state : deviceStates.values()) { if (deviceProfile != null) {
if (deviceProfile.getId().equals(state.getProfileId())) { for (DeviceState state : deviceStates.values()) {
state.updateProfile(ctx, deviceProfile); if (deviceProfile.getId().equals(state.getProfileId())) {
state.updateProfile(ctx, deviceProfile);
}
} }
} }
} }
@ -140,6 +149,12 @@ public class TbDeviceProfileNode implements TbNode {
} }
} }
@Override
public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) {
// Cleanup the cache for all entities that are no longer assigned to current server partitions
deviceStates.entrySet().removeIf(entry -> !ctx.isLocalEntity(entry.getKey()));
}
@Override @Override
public void destroy() { public void destroy() {
deviceStates.clear(); deviceStates.clear();
@ -150,7 +165,7 @@ public class TbDeviceProfileNode implements TbNode {
if (deviceState == null) { if (deviceState == null) {
DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId); DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId);
if (deviceProfile != null) { if (deviceProfile != null) {
deviceState = new DeviceState(ctx, config, deviceId, new DeviceProfileState(deviceProfile), rns); deviceState = new DeviceState(ctx, config, deviceId, new ProfileState(deviceProfile), rns);
deviceStates.put(deviceId, deviceState); deviceStates.put(deviceId, deviceState);
} }
} }

19
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -102,13 +102,16 @@ import { DeviceProfileAlarmComponent } from './profile/alarm/device-profile-alar
import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component'; import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component';
import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component'; import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component';
import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component'; import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component';
import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component';
import { FilterTextComponent } from './filter/filter-text.component'; import { FilterTextComponent } from './filter/filter-text.component';
import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component';
import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component'; import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component';
import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component'; import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component';
import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component'; import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component';
import { DeviceCredentialsComponent } from './device/device-credentials.component'; import { DeviceCredentialsComponent } from './device/device-credentials.component';
import { AlarmScheduleInfoComponent } from './profile/alarm/alarm-schedule-info.component';
import { AlarmScheduleDialogComponent } from '@home/components/profile/alarm/alarm-schedule-dialog.component';
import { EditAlarmDetailsDialogComponent } from './profile/alarm/edit-alarm-details-dialog.component';
import { AlarmRuleConditionDialogComponent } from '@home/components/profile/alarm/alarm-rule-condition-dialog.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -190,7 +193,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
DeviceProfileTransportConfigurationComponent, DeviceProfileTransportConfigurationComponent,
CreateAlarmRulesComponent, CreateAlarmRulesComponent,
AlarmRuleComponent, AlarmRuleComponent,
AlarmRuleKeyFiltersDialogComponent, AlarmRuleConditionDialogComponent,
AlarmRuleConditionComponent, AlarmRuleConditionComponent,
DeviceProfileAlarmComponent, DeviceProfileAlarmComponent,
DeviceProfileAlarmsComponent, DeviceProfileAlarmsComponent,
@ -198,9 +201,12 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
DeviceProfileDialogComponent, DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent, AddDeviceProfileDialogComponent,
RuleChainAutocompleteComponent, RuleChainAutocompleteComponent,
AlarmScheduleInfoComponent,
AlarmScheduleComponent, AlarmScheduleComponent,
DeviceWizardDialogComponent, DeviceWizardDialogComponent,
DeviceCredentialsComponent DeviceCredentialsComponent,
AlarmScheduleDialogComponent,
EditAlarmDetailsDialogComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -271,7 +277,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
DeviceProfileTransportConfigurationComponent, DeviceProfileTransportConfigurationComponent,
CreateAlarmRulesComponent, CreateAlarmRulesComponent,
AlarmRuleComponent, AlarmRuleComponent,
AlarmRuleKeyFiltersDialogComponent, AlarmRuleConditionDialogComponent,
AlarmRuleConditionComponent, AlarmRuleConditionComponent,
DeviceProfileAlarmComponent, DeviceProfileAlarmComponent,
DeviceProfileAlarmsComponent, DeviceProfileAlarmsComponent,
@ -281,7 +287,10 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
RuleChainAutocompleteComponent, RuleChainAutocompleteComponent,
DeviceWizardDialogComponent, DeviceWizardDialogComponent,
DeviceCredentialsComponent, DeviceCredentialsComponent,
AlarmScheduleComponent AlarmScheduleInfoComponent,
AlarmScheduleComponent,
AlarmScheduleDialogComponent,
EditAlarmDetailsDialogComponent
], ],
providers: [ providers: [
WidgetComponentService, WidgetComponentService,

124
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html

@ -0,0 +1,124 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="conditionFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary">
<h2>{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<tb-key-filter-list
[displayUserParameters]="false"
[allowUserDynamicSource]="false"
[telemetryKeysOnly]="true"
formControlName="keyFilters">
</tb-key-filter-list>
<section formGroupName="spec" class="row">
<mat-form-field class="mat-block" hideRequiredMarker>
<mat-label translate>device-profile.condition-type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="conditionFormGroup.get('spec.type').hasError('required')">
{{ 'device-profile.condition-type-required' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.DURATION">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<input type="number" required
step="1" min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
formControlName="value">
<mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('required')">
{{ 'device-profile.condition-duration-value-required' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('min')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('max')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('pattern')">
{{ 'device-profile.condition-duration-value-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<mat-select formControlName="unit"
required
placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="conditionFormGroup.get('spec.unit').hasError('required')">
{{ 'device-profile.condition-duration-time-unit-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.REPEATING">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<input type="number" required
step="1" min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-repeating-value' | translate }}"
formControlName="count">
<mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('required')">
{{ 'device-profile.condition-repeating-value-required' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('min')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('max')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('pattern')">
{{ 'device-profile.condition-repeating-value-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</section>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="(isLoading$ | async) || conditionFormGroup.invalid || !conditionFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
</div>
</form>

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyState.java → ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss

@ -13,10 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.rule.engine.profile; :host {
.row {
public class EntityKeyState { margin-top: 1em;
}
} }

150
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts

@ -0,0 +1,150 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models';
import { AlarmCondition, AlarmConditionType, AlarmConditionTypeTranslationMap } from '@shared/models/device.models';
import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
export interface AlarmRuleConditionDialogData {
readonly: boolean;
condition: AlarmCondition;
}
@Component({
selector: 'tb-alarm-rule-condition-dialog',
templateUrl: './alarm-rule-condition-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleConditionDialogComponent}],
styleUrls: ['/alarm-rule-condition-dialog.component.scss']
})
export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRuleConditionDialogComponent, AlarmCondition>
implements OnInit, ErrorStateMatcher {
timeUnits = Object.keys(TimeUnit);
timeUnitTranslations = timeUnitTranslationMap;
alarmConditionTypes = Object.keys(AlarmConditionType);
AlarmConditionType = AlarmConditionType;
alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap;
readonly = this.data.readonly;
condition = this.data.condition;
conditionFormGroup: FormGroup;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleConditionDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AlarmRuleConditionDialogComponent, AlarmCondition>,
private fb: FormBuilder,
private utils: UtilsService,
public translate: TranslateService) {
super(store, router, dialogRef);
this.conditionFormGroup = this.fb.group({
keyFilters: [keyFiltersToKeyFilterInfos(this.condition?.condition), Validators.required],
spec: this.fb.group({
type: [AlarmConditionType.SIMPLE, Validators.required],
unit: [{value: null, disable: true}, Validators.required],
value: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]],
count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]]
})
});
this.conditionFormGroup.patchValue({spec: this.condition?.spec});
this.conditionFormGroup.get('spec.type').valueChanges.subscribe((type) => {
this.updateValidators(type, true, true);
});
if (this.readonly) {
this.conditionFormGroup.disable({emitEvent: false});
} else {
this.updateValidators(this.condition?.spec?.type);
}
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) {
switch (type) {
case AlarmConditionType.DURATION:
this.conditionFormGroup.get('spec.value').enable();
this.conditionFormGroup.get('spec.unit').enable();
this.conditionFormGroup.get('spec.count').disable();
if (resetDuration) {
this.conditionFormGroup.get('spec').patchValue({
count: null
});
}
break;
case AlarmConditionType.REPEATING:
this.conditionFormGroup.get('spec.count').enable();
this.conditionFormGroup.get('spec.value').disable();
this.conditionFormGroup.get('spec.unit').disable();
if (resetDuration) {
this.conditionFormGroup.get('spec').patchValue({
value: null,
unit: null
});
}
break;
case AlarmConditionType.SIMPLE:
this.conditionFormGroup.get('spec.value').disable();
this.conditionFormGroup.get('spec.unit').disable();
this.conditionFormGroup.get('spec.count').disable();
if (resetDuration) {
this.conditionFormGroup.get('spec').patchValue({
value: null,
unit: null,
count: null
});
}
break;
}
this.conditionFormGroup.get('spec.value').updateValueAndValidity({emitEvent});
this.conditionFormGroup.get('spec.unit').updateValueAndValidity({emitEvent});
this.conditionFormGroup.get('spec.count').updateValueAndValidity({emitEvent});
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
this.condition = {
condition: keyFilterInfosToKeyFilters(this.conditionFormGroup.get('keyFilters').value),
spec: this.conditionFormGroup.get('spec').value
};
this.dialogRef.close(this.condition);
}
}

8
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html

@ -15,8 +15,9 @@
limitations under the License. limitations under the License.
--> -->
<div fxLayout="column" fxFlex> <div fxLayout="column" fxFlex [formGroup]="alarmRuleConditionFormGroup">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;"> <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<label class="tb-title" translate>device-profile.condition</label>
<span fxFlex></span> <span fxFlex></span>
<a mat-button color="primary" <a mat-button color="primary"
type="button" type="button"
@ -27,9 +28,12 @@
</a> </a>
</div> </div>
<div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)"> <div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)">
<tb-filter-text [formControl]="alarmRuleConditionControl" <tb-filter-text formControlName="condition"
required required
addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}"> addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
</tb-filter-text> </tb-filter-text>
<span class="tb-alarm-rule-condition-spec" [ngClass]="{disabled: this.disabled}" [innerHTML]="specText">
</span>
</div> </div>
</div> </div>

11
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss

@ -21,10 +21,15 @@
} }
} }
.tb-alarm-rule-condition { .tb-alarm-rule-condition {
padding: 8px;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
cursor: pointer; cursor: pointer;
.tb-alarm-rule-condition-spec {
margin-top: 1em;
line-height: 1.8em;
padding: 4px;
&.disabled {
opacity: 0.7;
}
}
} }
} }

85
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts

@ -18,20 +18,21 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
FormControl, FormControl, FormGroup,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
Validator Validator, Validators
} from '@angular/forms'; } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { KeyFilter } from '@shared/models/query/query.models'; import { deepClone, isUndefined } from '@core/utils';
import { deepClone } from '@core/utils';
import {
AlarmRuleKeyFiltersDialogComponent,
AlarmRuleKeyFiltersDialogData
} from './alarm-rule-key-filters-dialog.component';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { AlarmCondition, AlarmConditionSpec, AlarmConditionType } from '@shared/models/device.models';
import {
AlarmRuleConditionDialogComponent,
AlarmRuleConditionDialogData
} from '@home/components/profile/alarm/alarm-rule-condition-dialog.component';
import { TimeUnit } from '@shared/models/time/time.models';
@Component({ @Component({
selector: 'tb-alarm-rule-condition', selector: 'tb-alarm-rule-condition',
@ -55,9 +56,11 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
@Input() @Input()
disabled: boolean; disabled: boolean;
alarmRuleConditionControl: FormControl; alarmRuleConditionFormGroup: FormGroup;
specText = '';
private modelValue: Array<KeyFilter>; private modelValue: AlarmCondition;
private propagateChange = (v: any) => { }; private propagateChange = (v: any) => { };
@ -75,25 +78,31 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
} }
ngOnInit() { ngOnInit() {
this.alarmRuleConditionControl = this.fb.control(null); this.alarmRuleConditionFormGroup = this.fb.group({
condition: [null, Validators.required],
spec: [null, Validators.required]
});
} }
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) { if (this.disabled) {
this.alarmRuleConditionControl.disable({emitEvent: false}); this.alarmRuleConditionFormGroup.disable({emitEvent: false});
} else { } else {
this.alarmRuleConditionControl.enable({emitEvent: false}); this.alarmRuleConditionFormGroup.enable({emitEvent: false});
} }
} }
writeValue(value: Array<KeyFilter>): void { writeValue(value: AlarmCondition): void {
this.modelValue = value; this.modelValue = value;
if (this.modelValue !== null && isUndefined(this.modelValue?.spec)) {
this.modelValue = Object.assign(this.modelValue, {spec: {type: AlarmConditionType.SIMPLE}});
}
this.updateConditionInfo(); this.updateConditionInfo();
} }
public conditionSet() { public conditionSet() {
return this.modelValue && this.modelValue.length; return this.modelValue && this.modelValue.condition.length;
} }
public validate(c: FormControl) { public validate(c: FormControl) {
@ -108,13 +117,13 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
this.dialog.open<AlarmRuleKeyFiltersDialogComponent, AlarmRuleKeyFiltersDialogData, this.dialog.open<AlarmRuleConditionDialogComponent, AlarmRuleConditionDialogData,
Array<KeyFilter>>(AlarmRuleKeyFiltersDialogComponent, { AlarmCondition>(AlarmRuleConditionDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
readonly: this.disabled, readonly: this.disabled,
keyFilters: this.disabled ? this.modelValue : deepClone(this.modelValue) condition: this.disabled ? this.modelValue : deepClone(this.modelValue)
} }
}).afterClosed().subscribe((result) => { }).afterClosed().subscribe((result) => {
if (result) { if (result) {
@ -125,7 +134,45 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
} }
private updateConditionInfo() { private updateConditionInfo() {
this.alarmRuleConditionControl.patchValue(this.modelValue); this.alarmRuleConditionFormGroup.patchValue(
{
condition: this.modelValue?.condition,
spec: this.modelValue?.spec
}
);
this.updateSpecText();
}
private updateSpecText() {
this.specText = '';
if (this.modelValue && this.modelValue.spec) {
const spec = this.modelValue.spec;
switch (spec.type) {
case AlarmConditionType.SIMPLE:
break;
case AlarmConditionType.DURATION:
let duringText = '';
switch (spec.unit) {
case TimeUnit.SECONDS:
duringText = this.translate.instant('timewindow.seconds', {seconds: spec.value});
break;
case TimeUnit.MINUTES:
duringText = this.translate.instant('timewindow.minutes', {minutes: spec.value});
break;
case TimeUnit.HOURS:
duringText = this.translate.instant('timewindow.hours', {hours: spec.value});
break;
case TimeUnit.DAYS:
duringText = this.translate.instant('timewindow.days', {days: spec.value});
break;
}
this.specText = this.translate.instant('device-profile.condition-during', {during: duringText});
break;
case AlarmConditionType.REPEATING:
this.specText = this.translate.instant('device-profile.condition-repeat-times', {count: spec.count});
break;
}
}
} }
private updateModel() { private updateModel() {

118
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html

@ -16,90 +16,36 @@
--> -->
<div fxLayout="column" [formGroup]="alarmRuleFormGroup"> <div fxLayout="column" [formGroup]="alarmRuleFormGroup">
<mat-tab-group> <tb-alarm-rule-condition fxFlex class="row"
<mat-tab label="{{ 'device-profile.condition' | translate }}" formGroupName="condition"> formControlName="condition">
<tb-alarm-rule-condition fxFlex class="row" </tb-alarm-rule-condition>
formControlName="condition"> <mat-divider class="row"></mat-divider>
</tb-alarm-rule-condition> <tb-alarm-schedule-info fxFlex class="row"
<section formGroupName="spec" class="row"> formControlName="schedule">
<mat-form-field class="mat-block" hideRequiredMarker> </tb-alarm-schedule-info>
<mat-label translate>device-profile.condition-type</mat-label> <mat-divider class="row"></mat-divider>
<mat-select formControlName="type" required> <div fxLayout="column" fxFlex class="tb-alarm-rule-details row">
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType"> <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} <label class="tb-title" translate>device-profile.alarm-rule-details</label>
</mat-option> <span fxFlex></span>
</mat-select> <a mat-button color="primary"
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.type').hasError('required')"> *ngIf="!disabled"
{{ 'device-profile.condition-type-required' | translate }} type="button"
</mat-error> (click)="openEditDetailsDialog($event)"
</mat-form-field> matTooltip="{{ 'action.edit' | translate }}"
<div fxLayout="row" fxLayoutGap="8px" *ngIf="alarmRuleFormGroup.get('condition.spec.type').value == AlarmConditionType.DURATION"> matTooltipPosition="above">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always"> {{ 'action.edit' | translate }}
<mat-label></mat-label> </a>
<input type="number" required </div>
step="1" min="1" max="2147483647" matInput <div fxLayout="row" fxLayoutAlign="start start">
placeholder="{{ 'device-profile.condition-duration-value' | translate }}" <div class="tb-alarm-rule-details-content" [ngClass]="{disabled: this.disabled, collapsed: !this.expandAlarmDetails}"
formControlName="value"> (click)="!disabled ? openEditDetailsDialog($event) : {}"
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('required')"> fxFlex [innerHTML]="alarmRuleFormGroup.get('alarmDetails').value"></div>
{{ 'device-profile.condition-duration-value-required' | translate }} <a mat-button color="primary"
</mat-error> type="button"
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('min')"> (click)="expandAlarmDetails = !expandAlarmDetails">
{{ 'device-profile.condition-duration-value-range' | translate }} {{ (expandAlarmDetails ? 'action.hide' : 'action.read-more') | translate }}
</mat-error> </a>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('max')"> </div>
{{ 'device-profile.condition-duration-value-range' | translate }} </div>
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('pattern')">
{{ 'device-profile.condition-duration-value-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<mat-select formControlName="unit"
required
placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.unit').hasError('required')">
{{ 'device-profile.condition-duration-time-unit-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="8px" *ngIf="alarmRuleFormGroup.get('condition.spec.type').value == AlarmConditionType.REPEATING">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<input type="number" required
step="1" min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-repeating-value' | translate }}"
formControlName="count">
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('required')">
{{ 'device-profile.condition-repeating-value-required' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('min')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('max')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('pattern')">
{{ 'device-profile.condition-repeating-value-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</section>
</mat-tab>
<mat-tab label="{{ 'device-profile.schedule' | translate }}">
<tb-alarm-schedule fxFlex class="row"
formControlName="schedule">
</tb-alarm-schedule>
</mat-tab>
<mat-tab label="{{ 'device-profile.alarm-rule-details' | translate }}">
<mat-form-field class="mat-block row">
<mat-label translate>device-profile.alarm-details</mat-label>
<textarea matInput formControlName="alarmDetails" rows="5"></textarea>
</mat-form-field>
</mat-tab>
</mat-tab-group>
</div> </div>

24
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss

@ -17,5 +17,29 @@
.row { .row {
margin-top: 1em; margin-top: 1em;
} }
.tb-alarm-rule-details {
a.mat-button {
&:hover, &:focus {
border-bottom: none;
}
}
.tb-alarm-rule-details-content {
min-height: 33px;
overflow: hidden;
white-space: pre;
line-height: 1.8em;
padding: 4px;
cursor: pointer;
&.collapsed {
max-height: 33px;
white-space: nowrap;
text-overflow: ellipsis;
}
&.disabled {
opacity: 0.7;
cursor: auto;
}
}
}
} }

93
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts

@ -25,11 +25,14 @@ import {
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { AlarmConditionType, AlarmConditionTypeTranslationMap, AlarmRule } from '@shared/models/device.models'; import { AlarmRule } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { isUndefined } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import {
EditAlarmDetailsDialogComponent,
EditAlarmDetailsDialogData
} from '@home/components/profile/alarm/edit-alarm-details-dialog.component';
@Component({ @Component({
selector: 'tb-alarm-rule', selector: 'tb-alarm-rule',
@ -50,12 +53,6 @@ import { isUndefined } from '@core/utils';
}) })
export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validator {
timeUnits = Object.keys(TimeUnit);
timeUnitTranslations = timeUnitTranslationMap;
alarmConditionTypes = Object.keys(AlarmConditionType);
AlarmConditionType = AlarmConditionType;
alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap;
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -72,6 +69,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
alarmRuleFormGroup: FormGroup; alarmRuleFormGroup: FormGroup;
expandAlarmDetails = false;
private propagateChange = (v: any) => { }; private propagateChange = (v: any) => { };
constructor(private dialog: MatDialog, constructor(private dialog: MatDialog,
@ -87,21 +86,10 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
ngOnInit() { ngOnInit() {
this.alarmRuleFormGroup = this.fb.group({ this.alarmRuleFormGroup = this.fb.group({
condition: this.fb.group({ condition: [null, [Validators.required]],
condition: [null, Validators.required],
spec: this.fb.group({
type: [AlarmConditionType.SIMPLE, Validators.required],
unit: [{value: null, disable: true}, Validators.required],
value: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]],
count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]]
})
}, Validators.required),
schedule: [null], schedule: [null],
alarmDetails: [null] alarmDetails: [null]
}); });
this.alarmRuleFormGroup.get('condition.spec.type').valueChanges.subscribe((type) => {
this.updateValidators(type, true, true);
});
this.alarmRuleFormGroup.valueChanges.subscribe(() => { this.alarmRuleFormGroup.valueChanges.subscribe(() => {
this.updateModel(); this.updateModel();
}); });
@ -118,11 +106,25 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
writeValue(value: AlarmRule): void { writeValue(value: AlarmRule): void {
this.modelValue = value; this.modelValue = value;
if (this.modelValue !== null && isUndefined(this.modelValue?.condition?.spec)) {
this.modelValue = Object.assign(this.modelValue, {condition: {spec: {type: AlarmConditionType.SIMPLE}}});
}
this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false});
this.updateValidators(this.modelValue?.condition?.spec?.type); }
public openEditDetailsDialog($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<EditAlarmDetailsDialogComponent, EditAlarmDetailsDialogData,
string>(EditAlarmDetailsDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value
}
}).afterClosed().subscribe((alarmDetails) => {
if (isDefinedAndNotNull(alarmDetails)) {
this.alarmRuleFormGroup.patchValue({alarmDetails});
}
});
} }
public validate(c: FormControl) { public validate(c: FormControl) {
@ -133,47 +135,6 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
}; };
} }
private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) {
switch (type) {
case AlarmConditionType.DURATION:
this.alarmRuleFormGroup.get('condition.spec.value').enable();
this.alarmRuleFormGroup.get('condition.spec.unit').enable();
this.alarmRuleFormGroup.get('condition.spec.count').disable();
if (resetDuration) {
this.alarmRuleFormGroup.get('condition.spec').patchValue({
count: null
});
}
break;
case AlarmConditionType.REPEATING:
this.alarmRuleFormGroup.get('condition.spec.count').enable();
this.alarmRuleFormGroup.get('condition.spec.value').disable();
this.alarmRuleFormGroup.get('condition.spec.unit').disable();
if (resetDuration) {
this.alarmRuleFormGroup.get('condition.spec').patchValue({
value: null,
unit: null
});
}
break;
case AlarmConditionType.SIMPLE:
this.alarmRuleFormGroup.get('condition.spec.value').disable();
this.alarmRuleFormGroup.get('condition.spec.unit').disable();
this.alarmRuleFormGroup.get('condition.spec.count').disable();
if (resetDuration) {
this.alarmRuleFormGroup.get('condition.spec').patchValue({
value: null,
unit: null,
count: null
});
}
break;
}
this.alarmRuleFormGroup.get('condition.spec.value').updateValueAndValidity({emitEvent});
this.alarmRuleFormGroup.get('condition.spec.unit').updateValueAndValidity({emitEvent});
this.alarmRuleFormGroup.get('condition.spec.count').updateValueAndValidity({emitEvent});
}
private updateModel() { private updateModel() {
const value = this.alarmRuleFormGroup.value; const value = this.alarmRuleFormGroup.value;
if (this.modelValue) { if (this.modelValue) {

15
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html → ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html

@ -15,9 +15,9 @@
limitations under the License. limitations under the License.
--> -->
<form [formGroup]="keyFiltersFormGroup" (ngSubmit)="save()" style="width: 700px;"> <form [formGroup]="alarmScheduleFormGroup" (ngSubmit)="save()" style="width: 800px;">
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2>{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}</h2> <h2>{{ (readonly ? 'device-profile.schedule' : 'device-profile.edit-schedule') | translate }}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
@ -30,12 +30,9 @@
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column"> <div fxFlex fxLayout="column">
<tb-key-filter-list <tb-alarm-schedule
[displayUserParameters]="false" formControlName="alarmSchedule">
[allowUserDynamicSource]="false" </tb-alarm-schedule>
[telemetryKeysOnly]="true"
formControlName="keyFilters">
</tb-key-filter-list>
</div> </div>
</fieldset> </fieldset>
</div> </div>
@ -43,7 +40,7 @@
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
*ngIf="!readonly" *ngIf="!readonly"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || keyFiltersFormGroup.invalid || !keyFiltersFormGroup.dirty"> [disabled]="(isLoading$ | async) || alarmScheduleFormGroup.invalid || !alarmScheduleFormGroup.dirty">
{{ 'action.save' | translate }} {{ 'action.save' | translate }}
</button> </button>
<button mat-button color="primary" <button mat-button color="primary"

34
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.ts → ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.ts

@ -19,49 +19,49 @@ import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models'; import { AlarmSchedule } from '@shared/models/device.models';
export interface AlarmRuleKeyFiltersDialogData { export interface AlarmScheduleDialogData {
readonly: boolean; readonly: boolean;
keyFilters: Array<KeyFilter>; alarmSchedule: AlarmSchedule;
} }
@Component({ @Component({
selector: 'tb-alarm-rule-key-filters-dialog', selector: 'tb-alarm-schedule-dialog',
templateUrl: './alarm-rule-key-filters-dialog.component.html', templateUrl: './alarm-schedule-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleKeyFiltersDialogComponent}], providers: [{provide: ErrorStateMatcher, useExisting: AlarmScheduleDialogComponent}],
styleUrls: [] styleUrls: []
}) })
export class AlarmRuleKeyFiltersDialogComponent extends DialogComponent<AlarmRuleKeyFiltersDialogComponent, Array<KeyFilter>> export class AlarmScheduleDialogComponent extends DialogComponent<AlarmScheduleDialogComponent, AlarmSchedule>
implements OnInit, ErrorStateMatcher { implements OnInit, ErrorStateMatcher {
readonly = this.data.readonly; readonly = this.data.readonly;
keyFilters = this.data.keyFilters; alarmSchedule = this.data.alarmSchedule;
keyFiltersFormGroup: FormGroup; alarmScheduleFormGroup: FormGroup;
submitted = false; submitted = false;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleKeyFiltersDialogData, @Inject(MAT_DIALOG_DATA) public data: AlarmScheduleDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AlarmRuleKeyFiltersDialogComponent, Array<KeyFilter>>, public dialogRef: MatDialogRef<AlarmScheduleDialogComponent, AlarmSchedule>,
private fb: FormBuilder, private fb: FormBuilder,
private utils: UtilsService, private utils: UtilsService,
public translate: TranslateService) { public translate: TranslateService) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.keyFiltersFormGroup = this.fb.group({ this.alarmScheduleFormGroup = this.fb.group({
keyFilters: [keyFiltersToKeyFilterInfos(this.keyFilters), Validators.required] alarmSchedule: [this.alarmSchedule]
}); });
if (this.readonly) { if (this.readonly) {
this.keyFiltersFormGroup.disable({emitEvent: false}); this.alarmScheduleFormGroup.disable({emitEvent: false});
} }
} }
@ -80,7 +80,7 @@ export class AlarmRuleKeyFiltersDialogComponent extends DialogComponent<AlarmRul
save(): void { save(): void {
this.submitted = true; this.submitted = true;
this.keyFilters = keyFilterInfosToKeyFilters(this.keyFiltersFormGroup.get('keyFilters').value); this.alarmSchedule = this.alarmScheduleFormGroup.get('alarmSchedule').value;
this.dialogRef.close(this.keyFilters); this.dialogRef.close(this.alarmSchedule);
} }
} }

33
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.html

@ -0,0 +1,33 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div fxLayout="column" fxFlex>
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<label class="tb-title" translate>device-profile.schedule</label>
<span fxFlex></span>
<a mat-button color="primary"
type="button"
(click)="openScheduleDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
{{ (disabled ? 'action.view' : 'action.edit' ) | translate }}
</a>
</div>
<sapn class="tb-alarm-rule-schedule" [ngClass]="{disabled: this.disabled}" (click)="openScheduleDialog($event)"
[innerHTML]="scheduleText">
</sapn>
</div>

34
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.scss

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
display: flex;
a.mat-button {
&:hover, &:focus {
border-bottom: none;
}
}
.tb-alarm-rule-schedule {
line-height: 1.8em;
padding: 4px;
cursor: pointer;
&.disabled {
opacity: 0.7;
}
.nowrap {
white-space: nowrap;
}
}
}

137
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts

@ -0,0 +1,137 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
AlarmSchedule,
AlarmScheduleType,
AlarmScheduleTypeTranslationMap, dayOfWeekTranslations,
getAlarmScheduleRangeText, utcTimestampToTimeOfDay
} from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog';
import {
AlarmScheduleDialogComponent,
AlarmScheduleDialogData
} from '@home/components/profile/alarm/alarm-schedule-dialog.component';
import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-alarm-schedule-info',
templateUrl: './alarm-schedule-info.component.html',
styleUrls: ['./alarm-schedule-info.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AlarmScheduleInfoComponent),
multi: true
}]
})
export class AlarmScheduleInfoComponent implements ControlValueAccessor, OnInit {
@Input()
disabled: boolean;
private modelValue: AlarmSchedule;
scheduleText = '';
private propagateChange = (v: any) => { };
constructor(private dialog: MatDialog,
private translate: TranslateService,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: AlarmSchedule): void {
this.modelValue = value;
this.updateScheduleText();
}
private updateScheduleText() {
let schedule = this.modelValue;
if (!isDefinedAndNotNull(schedule)) {
schedule = {
type: AlarmScheduleType.ANY_TIME
};
}
this.scheduleText = '';
switch (schedule.type) {
case AlarmScheduleType.ANY_TIME:
this.scheduleText = this.translate.instant('device-profile.schedule-any-time');
break;
case AlarmScheduleType.SPECIFIC_TIME:
for (const day of schedule.daysOfWeek) {
if (this.scheduleText.length) {
this.scheduleText += ', ';
}
this.scheduleText += this.translate.instant(dayOfWeekTranslations[day - 1]);
}
this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(schedule.startsOn),
utcTimestampToTimeOfDay(schedule.endsOn)) + '</b>';
break;
case AlarmScheduleType.CUSTOM:
for (const item of schedule.items) {
if (item.enabled) {
if (this.scheduleText.length) {
this.scheduleText += '<br/>';
}
this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]);
this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn),
utcTimestampToTimeOfDay(item.endsOn)) + '</b>';
}
}
break;
}
}
public openScheduleDialog($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<AlarmScheduleDialogComponent, AlarmScheduleDialogData,
AlarmSchedule>(AlarmScheduleDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
readonly: this.disabled,
alarmSchedule: this.disabled ? this.modelValue : deepClone(this.modelValue)
}
}).afterClosed().subscribe((result) => {
if (result) {
this.modelValue = result;
this.propagateChange(this.modelValue);
this.updateScheduleText();
this.cd.detectChanges();
}
});
}
}

191
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html

@ -35,33 +35,20 @@
</tb-timezone-select> </tb-timezone-select>
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME"> <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME">
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div> <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
<div fxLayout="column" fxLayout.gt-md="row" fxLayoutGap="16px" style="padding-bottom: 16px;"> <div fxLayout="column" fxLayout.gt-md="row" fxLayoutGap="16px">
<div fxLayout="row" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutGap="16px">
<mat-checkbox [formControl]="weeklyRepeatControl(0)"> <mat-checkbox *ngFor="let day of firstRowDays" [formControl]="weeklyRepeatControl(day)">
{{ 'device-profile.schedule-day.monday' | translate }} {{ dayOfWeekTranslationsArray[day] | translate }}
</mat-checkbox>
<mat-checkbox [formControl]="weeklyRepeatControl(1)">
{{ 'device-profile.schedule-day.tuesday' | translate }}
</mat-checkbox>
<mat-checkbox [formControl]="weeklyRepeatControl(2)">
{{ 'device-profile.schedule-day.wednesday' | translate }}
</mat-checkbox>
<mat-checkbox [formControl]="weeklyRepeatControl(3)">
{{ 'device-profile.schedule-day.thursday' | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
<div fxLayout="row" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutGap="16px">
<mat-checkbox [formControl]="weeklyRepeatControl(4)"> <mat-checkbox *ngFor="let day of secondRowDays" [formControl]="weeklyRepeatControl(day)">
{{ 'device-profile.schedule-day.friday' | translate }} {{ dayOfWeekTranslationsArray[day] | translate }}
</mat-checkbox>
<mat-checkbox [formControl]="weeklyRepeatControl(5)">
{{ 'device-profile.schedule-day.saturday' | translate }}
</mat-checkbox>
<mat-checkbox [formControl]="weeklyRepeatControl(6)">
{{ 'device-profile.schedule-day.sunday' | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</div> </div>
<tb-error style="display: block;" [error]="alarmScheduleForm.get('daysOfWeek').hasError('dayOfWeeks')
? ('device-profile.schedule-days-of-week-required' | translate) : ''"></tb-error>
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-time</div> <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-time</div>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<div fxLayout="row" fxLayoutGap="8px" fxFlex.gt-md> <div fxLayout="row" fxLayoutGap="8px" fxFlex.gt-md>
@ -87,169 +74,35 @@
</section> </section>
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM"> <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM">
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div> <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
<div fxLayout="column" formArrayName="items" fxLayoutGap="1em">
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="0" fxLayoutAlign="start center" fxLayoutAlign.xs="center start"> <div *ngFor="let day of allDays" fxLayout="column" formArrayName="items" fxLayoutGap="1em">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 0)"> <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" [formGroupName]="''+day" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
{{ 'device-profile.schedule-day.monday' | translate }} <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, day)">
</mat-checkbox> {{ dayOfWeekTranslationsArray[day] | translate }}
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker1" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker1 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker1">
</mat-form-field>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker1" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker1 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker1">
</mat-form-field>
</div>
<div fxFlex fxLayoutAlign="center center"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(0))">
</div>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="1" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 1)">
{{ 'device-profile.schedule-day.tuesday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker2" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker2 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker2">
</mat-form-field>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker2" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker2 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker2">
</mat-form-field>
</div>
<div fxFlex fxLayoutAlign="center center"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(1))">
</div>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="2" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 2)">
{{ 'device-profile.schedule-day.wednesday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker3" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker3 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker3">
</mat-form-field>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker3" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker3 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker3">
</mat-form-field>
</div>
<div fxFlex fxLayoutAlign="center center"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(2))">
</div>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="3" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 3)">
{{ 'device-profile.schedule-day.thursday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker4" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker4 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker4">
</mat-form-field>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker4" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker4 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker4">
</mat-form-field>
</div>
<div fxFlex fxLayoutAlign="center center"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(3))">
</div>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="4" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 4)">
{{ 'device-profile.schedule-day.friday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker5" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker5 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker5">
</mat-form-field>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker5" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker5 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker5">
</mat-form-field>
</div>
<div fxFlex fxLayoutAlign="center center"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(4))">
</div>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="5" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 5)">
{{ 'device-profile.schedule-day.saturday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker6" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker6 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker6">
</mat-form-field>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker6" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker6 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker6">
</mat-form-field>
</div>
<div fxFlex fxLayoutAlign="center center"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(5))">
</div>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="6" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 6)">
{{ 'device-profile.schedule-day.sunday' | translate }}
</mat-checkbox> </mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex> <div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label> <mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker7" matPrefix></mat-datetimepicker-toggle> <mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker7 type="time" openOnFocus="true"></mat-datetimepicker> <mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker7"> <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker">
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label> <mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker7" matPrefix></mat-datetimepicker-toggle> <mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker7 type="time" openOnFocus="true"></mat-datetimepicker> <mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker7"> <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker">
</mat-form-field> </mat-form-field>
</div> </div>
<div fxFlex fxLayoutAlign="center center" <div fxFlex fxLayoutAlign="center center"
style="text-align: center" style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(6))"> [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))">
</div> </div>
</div> </div>
</div> </div>
<tb-error style="display: block;" [error]="alarmScheduleForm.get('items').hasError('dayOfWeeks')
? ('device-profile.schedule-days-of-week-required' | translate) : ''"></tb-error>
</section> </section>
</div> </div>
</section> </section>

83
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts

@ -28,7 +28,12 @@ import {
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { AlarmSchedule, AlarmScheduleType, AlarmScheduleTypeTranslationMap } from '@shared/models/device.models'; import {
AlarmSchedule,
AlarmScheduleType,
AlarmScheduleTypeTranslationMap,
dayOfWeekTranslations, getAlarmScheduleRangeText, timeOfDayToUTCTimestamp, utcTimestampToTimeOfDay
} from '@shared/models/device.models';
import { isDefined, isDefinedAndNotNull } from '@core/utils'; import { isDefined, isDefinedAndNotNull } from '@core/utils';
import * as _moment from 'moment-timezone'; import * as _moment from 'moment-timezone';
import { MatCheckboxChange } from '@angular/material/checkbox'; import { MatCheckboxChange } from '@angular/material/checkbox';
@ -59,11 +64,18 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
alarmScheduleType = AlarmScheduleType; alarmScheduleType = AlarmScheduleType;
alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap; alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap;
dayOfWeekTranslationsArray = dayOfWeekTranslations;
allDays = Array(7).fill(0).map((x, i) => i);
firstRowDays = Array(4).fill(0).map((x, i) => i);
secondRowDays = Array(3).fill(0).map((x, i) => i + 4);
private modelValue: AlarmSchedule; private modelValue: AlarmSchedule;
private defaultItems = Array.from({length: 7}, (value, i) => ({ private defaultItems = Array.from({length: 7}, (value, i) => ({
enabled: true, enabled: true,
dayOfWeek: i dayOfWeek: i + 1
})); }));
private propagateChange = (v: any) => { }; private propagateChange = (v: any) => { };
@ -75,10 +87,10 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
this.alarmScheduleForm = this.fb.group({ this.alarmScheduleForm = this.fb.group({
type: [AlarmScheduleType.ANY_TIME, Validators.required], type: [AlarmScheduleType.ANY_TIME, Validators.required],
timezone: [null, Validators.required], timezone: [null, Validators.required],
daysOfWeek: this.fb.array(new Array(7).fill(false)), daysOfWeek: this.fb.array(new Array(7).fill(false), this.validateDayOfWeeks),
startsOn: [0, Validators.required], startsOn: [0, Validators.required],
endsOn: [0, Validators.required], endsOn: [0, Validators.required],
items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i))) items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems)
}); });
this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => { this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => {
this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: this.defaultTimezone}, {emitEvent: false}); this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: this.defaultTimezone}, {emitEvent: false});
@ -90,6 +102,26 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
}); });
} }
validateDayOfWeeks(control: AbstractControl): ValidationErrors | null {
const dayOfWeeks: boolean[] = control.value;
if (!dayOfWeeks || !dayOfWeeks.length || !dayOfWeeks.find(v => v === true)) {
return {
dayOfWeeks: true
};
}
return null;
}
validateItems(control: AbstractControl): ValidationErrors | null {
const items: any[] = control.value;
if (!items || !items.length || !items.find(v => v.enabled === true)) {
return {
dayOfWeeks: true
};
}
return null;
}
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.propagateChange = fn; this.propagateChange = fn;
} }
@ -123,8 +155,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
type: this.modelValue.type, type: this.modelValue.type,
timezone: this.modelValue.timezone, timezone: this.modelValue.timezone,
daysOfWeek, daysOfWeek,
startsOn: this.timestampToTime(this.modelValue.startsOn), startsOn: utcTimestampToTimeOfDay(this.modelValue.startsOn),
endsOn: this.timestampToTime(this.modelValue.endsOn) endsOn: utcTimestampToTimeOfDay(this.modelValue.endsOn)
}, {emitEvent: false}); }, {emitEvent: false});
break; break;
case AlarmScheduleType.CUSTOM: case AlarmScheduleType.CUSTOM:
@ -136,8 +168,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
this.disabledSelectedTime(item.enabled, index); this.disabledSelectedTime(item.enabled, index);
alarmDays.push({ alarmDays.push({
enabled: item.enabled, enabled: item.enabled,
startsOn: this.timestampToTime(item.startsOn), startsOn: utcTimestampToTimeOfDay(item.startsOn),
endsOn: this.timestampToTime(item.endsOn) endsOn: utcTimestampToTimeOfDay(item.endsOn)
}); });
}); });
this.alarmScheduleForm.patchValue({ this.alarmScheduleForm.patchValue({
@ -202,15 +234,15 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
.filter(day => !!day); .filter(day => !!day);
} }
if (isDefined(value.startsOn) && value.startsOn !== 0) { if (isDefined(value.startsOn) && value.startsOn !== 0) {
value.startsOn = this.timeToTimestampUTC(value.startsOn); value.startsOn = timeOfDayToUTCTimestamp(value.startsOn);
} }
if (isDefined(value.endsOn) && value.endsOn !== 0) { if (isDefined(value.endsOn) && value.endsOn !== 0) {
value.endsOn = this.timeToTimestampUTC(value.endsOn); value.endsOn = timeOfDayToUTCTimestamp(value.endsOn);
} }
if (isDefined(value.items)){ if (isDefined(value.items)){
value.items = this.alarmScheduleForm.getRawValue().items; value.items = this.alarmScheduleForm.getRawValue().items;
value.items = value.items.map((item) => { value.items = value.items.map((item) => {
return { ...item, startsOn: this.timeToTimestampUTC(item.startsOn), endsOn: this.timeToTimestampUTC(item.endsOn)}; return { ...item, startsOn: timeOfDayToUTCTimestamp(item.startsOn), endsOn: timeOfDayToUTCTimestamp(item.endsOn)};
}); });
} }
this.modelValue = value; this.modelValue = value;
@ -218,21 +250,11 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
} }
} }
private timeToTimestampUTC(date: Date | number): number {
if (typeof date === 'number' || date === null) {
return 0;
}
return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf();
}
private timestampToTime(time = 0): Date {
return new Date(time + new Date().getTimezoneOffset() * 60 * 1000);
}
private defaultItemsScheduler(index): FormGroup { private defaultItemsScheduler(index): FormGroup {
return this.fb.group({ return this.fb.group({
enabled: [true], enabled: [true],
dayOfWeek: [index], dayOfWeek: [index + 1],
startsOn: [0, Validators.required], startsOn: [0, Validators.required],
endsOn: [0, Validators.required] endsOn: [0, Validators.required]
}); });
@ -253,23 +275,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
} }
} }
private timeToMoment(date: Date | number): _moment.Moment {
if (typeof date === 'number' || date === null) {
return _moment([1970, 0, 1, 0, 0, 0, 0]);
}
return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 0, 0]);
}
getSchedulerRangeText(control: FormGroup | AbstractControl): string { getSchedulerRangeText(control: FormGroup | AbstractControl): string {
const start = this.timeToMoment(control.get('startsOn').value); return getAlarmScheduleRangeText(control.get('startsOn').value, control.get('endsOn').value);
const end = this.timeToMoment(control.get('endsOn').value);
if (start < end) {
return `<span><span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">${end.format('hh:mm A')}</span></span>`;
} else if (start.valueOf() === 0 && end.valueOf() === 0 || start.isSame(_moment([1970, 0])) && end.isSame(_moment([1970, 0]))) {
return '<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">12:00 PM</span></span>';
}
return `<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">${end.format('hh:mm A')}</span>` +
` and <span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">12:00 PM</span></span>`;
} }
get itemsSchedulerForm(): FormArray { get itemsSchedulerForm(): FormArray {

1
ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss

@ -19,6 +19,7 @@
border: 2px groove rgba(0, 0, 0, .45); border: 2px groove rgba(0, 0, 0, .45);
border-radius: 4px; border-radius: 4px;
padding: 8px; padding: 8px;
min-width: 0;
} }
} }

2
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html

@ -56,7 +56,7 @@
<mat-checkbox formControlName="propagate" style="display: block; padding-bottom: 16px;"> <mat-checkbox formControlName="propagate" style="display: block; padding-bottom: 16px;">
{{ 'device-profile.propagate-alarm' | translate }} {{ 'device-profile.propagate-alarm' | translate }}
</mat-checkbox> </mat-checkbox>
<section *ngIf="alarmFormGroup.get('propagate').value === true"> <section *ngIf="alarmFormGroup.get('propagate').value === true" style="padding-bottom: 1em;">
<mat-form-field floatLabel="always" class="mat-block"> <mat-form-field floatLabel="always" class="mat-block">
<mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label> <mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label>
<mat-chip-list #relationTypesChipList [disabled]="disabled"> <mat-chip-list #relationTypesChipList [disabled]="disabled">

1
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts

@ -57,6 +57,7 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
@Input()
expanded = false; expanded = false;
private modelValue: DeviceProfileAlarm; private modelValue: DeviceProfileAlarm;

3
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html

@ -21,6 +21,7 @@
let $index = index; last as isLast;" let $index = index; last as isLast;"
fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}"> fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<tb-device-profile-alarm [formControl]="alarmControl" <tb-device-profile-alarm [formControl]="alarmControl"
[expanded]="$index === 0"
(removeAlarm)="removeAlarm($index)"> (removeAlarm)="removeAlarm($index)">
</tb-device-profile-alarm> </tb-device-profile-alarm>
</div> </div>
@ -29,7 +30,7 @@
<span translate fxLayoutAlign="center center" <span translate fxLayoutAlign="center center"
class="tb-prompt">device-profile.no-alarm-rules</span> class="tb-prompt">device-profile.no-alarm-rules</span>
</div> </div>
<div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="end center" <div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="start center"
style="padding-top: 16px;"> style="padding-top: 16px;">
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
type="button" type="button"

53
ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html

@ -0,0 +1,53 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="editDetailsFormGroup" (ngSubmit)="save()" style="width: 800px;">
<mat-toolbar color="primary">
<h2>{{ 'device-profile.alarm-rule-details' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<mat-form-field class="mat-block">
<mat-label translate>device-profile.alarm-details</mat-label>
<textarea matInput formControlName="alarmDetails" rows="5"></textarea>
</mat-form-field>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

80
ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts

@ -0,0 +1,80 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
export interface EditAlarmDetailsDialogData {
alarmDetails: string;
}
@Component({
selector: 'tb-edit-alarm-details-dialog',
templateUrl: './edit-alarm-details-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: EditAlarmDetailsDialogComponent}],
styleUrls: []
})
export class EditAlarmDetailsDialogComponent extends DialogComponent<EditAlarmDetailsDialogComponent, string>
implements OnInit, ErrorStateMatcher {
alarmDetails = this.data.alarmDetails;
editDetailsFormGroup: FormGroup;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: EditAlarmDetailsDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<EditAlarmDetailsDialogComponent, string>,
private fb: FormBuilder,
private utils: UtilsService,
public translate: TranslateService) {
super(store, router, dialogRef);
this.editDetailsFormGroup = this.fb.group({
alarmDetails: [this.alarmDetails]
});
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
this.alarmDetails = this.editDetailsFormGroup.get('alarmDetails').value;
this.dialogRef.close(this.alarmDetails);
}
}

4
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts

@ -266,11 +266,11 @@ export class DeviceWizardDialogComponent extends
}) })
); );
} else { } else {
return of(null); return of(this.deviceWizardFormGroup.get('deviceProfileId').value);
} }
} }
private createDevice(profileId: EntityId = this.deviceWizardFormGroup.get('deviceProfileId').value): Observable<BaseData<HasId>> { private createDevice(profileId): Observable<BaseData<HasId>> {
const device = { const device = {
name: this.deviceWizardFormGroup.get('name').value, name: this.deviceWizardFormGroup.get('name').value,
label: this.deviceWizardFormGroup.get('label').value, label: this.deviceWizardFormGroup.get('label').value,

28
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html

@ -61,29 +61,7 @@
</div> </div>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab *ngIf="entity && !isEdit" <mat-tab *ngIf="entity"
label="{{ 'attribute.attributes' | translate }}" #attributesTab="matTab"> label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-attribute-table [defaultAttributeScope]="attributeScopes.SERVER_SCOPE" <tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
[active]="attributesTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity && !isEdit"
label="{{ 'attribute.latest-telemetry' | translate }}" #telemetryTab="matTab">
<tb-attribute-table [defaultAttributeScope]="latestTelemetryTypes.LATEST_TELEMETRY"
disableAttributeScopeSelection
[active]="telemetryTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity && !isEdit"
label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
<tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>
</mat-tab>
<mat-tab *ngIf="entity && !isEdit"
label="{{ 'tenant.events' | translate }}" #eventsTab="matTab">
<tb-event-table [defaultEventType]="eventTypes.ERROR" [active]="eventsTab.isActive" [tenantId]="nullUid"
[entityId]="entity.id"></tb-event-table>
</mat-tab> </mat-tab>

61
ui-ngx/src/app/shared/models/device.models.ts

@ -25,6 +25,8 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { EntityInfoData } from '@shared/models/entity.models'; import { EntityInfoData } from '@shared/models/entity.models';
import { KeyFilter } from '@shared/models/query/query.models'; import { KeyFilter } from '@shared/models/query/query.models';
import { TimeUnit } from '@shared/models/time/time.models'; import { TimeUnit } from '@shared/models/time/time.models';
import * as _moment from 'moment-timezone';
import { AbstractControl, FormGroup } from '@angular/forms';
export enum DeviceProfileType { export enum DeviceProfileType {
DEFAULT = 'DEFAULT' DEFAULT = 'DEFAULT'
@ -408,3 +410,62 @@ export interface ClaimResult {
device: Device; device: Device;
response: ClaimResponse; response: ClaimResponse;
} }
export const dayOfWeekTranslations = new Array<string>(
'device-profile.schedule-day.monday',
'device-profile.schedule-day.tuesday',
'device-profile.schedule-day.wednesday',
'device-profile.schedule-day.thursday',
'device-profile.schedule-day.friday',
'device-profile.schedule-day.saturday',
'device-profile.schedule-day.sunday'
);
export function getDayString(day: number): string {
switch (day) {
case 0:
return 'device-profile.schedule-day.monday';
case 1:
return this.translate.instant('device-profile.schedule-day.tuesday');
case 2:
return this.translate.instant('device-profile.schedule-day.wednesday');
case 3:
return this.translate.instant('device-profile.schedule-day.thursday');
case 4:
return this.translate.instant('device-profile.schedule-day.friday');
case 5:
return this.translate.instant('device-profile.schedule-day.saturday');
case 6:
return this.translate.instant('device-profile.schedule-day.sunday');
}
}
export function timeOfDayToUTCTimestamp(date: Date | number): number {
if (typeof date === 'number' || date === null) {
return 0;
}
return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf();
}
export function utcTimestampToTimeOfDay(time = 0): Date {
return new Date(time + new Date().getTimezoneOffset() * 60 * 1000);
}
function timeOfDayToMoment(date: Date | number): _moment.Moment {
if (typeof date === 'number' || date === null) {
return _moment([1970, 0, 1, 0, 0, 0, 0]);
}
return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 0, 0]);
}
export function getAlarmScheduleRangeText(startsOn: Date | number, endsOn: Date | number): string {
const start = timeOfDayToMoment(startsOn);
const end = timeOfDayToMoment(endsOn);
if (start < end) {
return `<span><span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">${end.format('hh:mm A')}</span></span>`;
} else if (start.valueOf() === 0 && end.valueOf() === 0 || start.isSame(_moment([1970, 0])) && end.isSame(_moment([1970, 0]))) {
return '<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">12:00 PM</span></span>';
}
return `<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">${end.format('hh:mm A')}</span>` +
` and <span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">12:00 PM</span></span>`;
}

32
ui-ngx/src/app/shared/models/query/query.models.ts

@ -476,21 +476,23 @@ export function keyFilterInfosToKeyFilters(keyFilterInfos: Array<KeyFilterInfo>)
export function keyFiltersToKeyFilterInfos(keyFilters: Array<KeyFilter>): Array<KeyFilterInfo> { export function keyFiltersToKeyFilterInfos(keyFilters: Array<KeyFilter>): Array<KeyFilterInfo> {
const keyFilterInfos: Array<KeyFilterInfo> = []; const keyFilterInfos: Array<KeyFilterInfo> = [];
const keyFilterInfoMap: {[infoKey: string]: KeyFilterInfo} = {}; const keyFilterInfoMap: {[infoKey: string]: KeyFilterInfo} = {};
for (const keyFilter of keyFilters) { if (keyFilters) {
const key = keyFilter.key; for (const keyFilter of keyFilters) {
const infoKey = key.key + key.type + keyFilter.valueType; const key = keyFilter.key;
let keyFilterInfo = keyFilterInfoMap[infoKey]; const infoKey = key.key + key.type + keyFilter.valueType;
if (!keyFilterInfo) { let keyFilterInfo = keyFilterInfoMap[infoKey];
keyFilterInfo = { if (!keyFilterInfo) {
key, keyFilterInfo = {
valueType: keyFilter.valueType, key,
predicates: [] valueType: keyFilter.valueType,
}; predicates: []
keyFilterInfoMap[infoKey] = keyFilterInfo; };
keyFilterInfos.push(keyFilterInfo); keyFilterInfoMap[infoKey] = keyFilterInfo;
} keyFilterInfos.push(keyFilterInfo);
if (keyFilter.predicate) { }
keyFilterInfo.predicates.push(keyFilterPredicateToKeyFilterPredicateInfo(keyFilter.predicate)); if (keyFilter.predicate) {
keyFilterInfo.predicates.push(keyFilterPredicateToKeyFilterPredicateInfo(keyFilter.predicate));
}
} }
} }
return keyFilterInfos; return keyFilterInfos;

10
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -55,7 +55,9 @@
"continue": "Continue", "continue": "Continue",
"discard-changes": "Discard Changes", "discard-changes": "Discard Changes",
"download": "Download", "download": "Download",
"next-with-label": "Next: {{label}}" "next-with-label": "Next: {{label}}",
"read-more": "Read more",
"hide": "Hide"
}, },
"aggregation": { "aggregation": {
"aggregation": "Aggregation", "aggregation": "Aggregation",
@ -932,15 +934,18 @@
"condition-type": "Condition type", "condition-type": "Condition type",
"condition-type-simple": "Simple", "condition-type-simple": "Simple",
"condition-type-duration": "Duration", "condition-type-duration": "Duration",
"condition-during": "During <b>{{during}}</b>",
"condition-type-repeating": "Repeating", "condition-type-repeating": "Repeating",
"condition-type-required": "Condition type is required.", "condition-type-required": "Condition type is required.",
"condition-repeating-value": "Count of events", "condition-repeating-value": "Count of events",
"condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.",
"condition-repeating-value-pattern": "Count of events should be integers.", "condition-repeating-value-pattern": "Count of events should be integers.",
"condition-repeating-value-required": "Count of events is required.", "condition-repeating-value-required": "Count of events is required.",
"condition-repeat-times": "Repeats <b>{ count, plural, 1 {1 time} other {# times} }</b>",
"schedule-type": "Scheduler type", "schedule-type": "Scheduler type",
"schedule-type-required": "Scheduler type is required.", "schedule-type-required": "Scheduler type is required.",
"schedule": "Schedule", "schedule": "Schedule",
"edit-schedule": "Edit alarm schedule",
"schedule-any-time": "Active all the time", "schedule-any-time": "Active all the time",
"schedule-specific-time": "Active at a specific time", "schedule-specific-time": "Active at a specific time",
"schedule-custom": "Custom", "schedule-custom": "Custom",
@ -956,7 +961,8 @@
"schedule-days": "Days", "schedule-days": "Days",
"schedule-time": "Time", "schedule-time": "Time",
"schedule-time-from": "From", "schedule-time-from": "From",
"schedule-time-to": "To" "schedule-time-to": "To",
"schedule-days-of-week-required": "At least one day of week should be selected."
}, },
"dialog": { "dialog": {
"close": "Close dialog" "close": "Close dialog"

Loading…
Cancel
Save