diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 402caed226..b02bc302de 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/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); } + @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) { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("ruleNodeId", ruleNodeId.toString()); diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index 5c54c0232e..6d80c05c05 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/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()); + tbClusterService.onEntityStateChange(savedRuleChain.getTenantId(), savedRuleChain.getId(), ComponentLifecycleEvent.CREATED); + logEntityAction(savedRuleChain.getId(), savedRuleChain, null, ActionType.ADDED, null); return savedRuleChain; diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index f432851ffc..ac047060ae 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/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.Path; import java.nio.file.Paths; +import java.util.Optional; import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper; @@ -243,6 +244,11 @@ public class InstallScripts { try { JsonNode oauth2ConfigTemplateJson = objectMapper.readTree(path.toFile()); OAuth2ClientRegistrationTemplate clientRegistrationTemplate = objectMapper.treeToValue(oauth2ConfigTemplateJson, OAuth2ClientRegistrationTemplate.class); + Optional existingClientRegistrationTemplate = + oAuth2TemplateService.findClientRegistrationTemplateByProviderId(clientRegistrationTemplate.getProviderId()); + if (existingClientRegistrationTemplate.isPresent()) { + clientRegistrationTemplate.setId(existingClientRegistrationTemplate.get().getId()); + } oAuth2TemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate); } catch (Exception e) { log.error("Unable to load oauth2 config templates from json: [{}]", path.toString()); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateService.java index b3718a8d92..54a47d9fdb 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateService.java +++ b/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 java.util.List; +import java.util.Optional; public interface OAuth2ConfigTemplateService { OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate); + Optional findClientRegistrationTemplateByProviderId(String providerId); + OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId); List findAllClientRegistrationTemplates(); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateService.java index 07138a1a11..d5ad9bbbb6 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateService.java +++ b/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); + void removeByRuleNodeId(TenantId tenantId, RuleNodeId selfId); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValueSourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValueSourceType.java index 96734fde92..7da9ab00af 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValueSourceType.java +++ b/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 { CURRENT_TENANT, CURRENT_CUSTOMER, - CURRENT_USER + CURRENT_USER, + CURRENT_DEVICE } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationTemplateDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationTemplateDao.java index 75fcbf1b10..6c377bc067 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationTemplateDao.java +++ b/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 java.util.List; +import java.util.Optional; public interface OAuth2ClientRegistrationTemplateDao extends Dao { + + Optional findByProviderId(String providerId); + List findAll(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java index f6fab8e38e..381d704b51 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java +++ b/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 java.util.List; +import java.util.Optional; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -37,6 +38,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString; @Service 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_PROVIDER_ID = "Incorrect clientRegistrationProviderId "; @Autowired private OAuth2ClientRegistrationTemplateDao clientRegistrationTemplateDao; @@ -59,6 +61,13 @@ public class OAuth2ConfigTemplateServiceImpl extends AbstractEntityService imple return savedClientRegistrationTemplate; } + @Override + public Optional findClientRegistrationTemplateByProviderId(String providerId) { + log.trace("Executing findClientRegistrationTemplateByProviderId [{}]", providerId); + validateString(providerId, INCORRECT_CLIENT_REGISTRATION_PROVIDER_ID + providerId); + return clientRegistrationTemplateDao.findByProviderId(providerId); + } + @Override public OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId) { log.trace("Executing findClientRegistrationTemplateById [{}]", templateId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java index a0b83f0333..1d7e8af03f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java +++ b/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); } + @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) { try { if (update) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java index b12c448aa5..89c2baea7f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeStateDao.java @@ -16,6 +16,8 @@ package org.thingsboard.server.dao.rule; 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.PageLink; import org.thingsboard.server.common.data.rule.RuleNodeState; @@ -31,4 +33,6 @@ public interface RuleNodeStateDao extends Dao { PageData findByRuleNodeId(UUID ruleNodeId, PageLink pageLink); RuleNodeState findByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId); + + void removeByRuleNodeId(UUID ruleNodeId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationTemplateDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationTemplateDao.java index 70c3c03479..4540a9b3d1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationTemplateDao.java +++ b/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.List; +import java.util.Optional; import java.util.UUID; @Component @@ -43,6 +44,12 @@ public class JpaOAuth2ClientRegistrationTemplateDao extends JpaAbstractDao findByProviderId(String providerId) { + OAuth2ClientRegistrationTemplate oAuth2ClientRegistrationTemplate = DaoUtil.getData(repository.findByProviderId(providerId)); + return Optional.ofNullable(oAuth2ClientRegistrationTemplate); + } + @Override public List findAll() { Iterable entities = repository.findAll(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationTemplateRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationTemplateRepository.java index 8ffd380496..cba85280d7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationTemplateRepository.java +++ b/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; public interface OAuth2ClientRegistrationTemplateRepository extends CrudRepository { + + OAuth2ClientRegistrationTemplateEntity findByProviderId(String providerId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index 232a534e88..75f7658a5a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -472,13 +472,13 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { if (entityFilter.isFetchLastLevelOnly()) { String fromOrTo = (entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "from" : "to"); 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(" and ") .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(")"); whereFilter += " and ( re.lvl = " + entityFilter.getMaxLevel() + " OR " + notExistsPart.toString() + ")"; } @@ -551,12 +551,12 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { StringBuilder notExistsPart = new StringBuilder(); notExistsPart.append(" NOT EXISTS (SELECT 1 from relation nr WHERE "); - notExistsPart.append(whereFilter.toString()); notExistsPart - .append(" and ") .append("nr.").append(fromOrTo).append("_id").append(" = re.").append(toOrFrom).append("_id") .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(")"); whereFilter.append(" and ( re.lvl = ").append(entityFilter.getMaxLevel()).append(" OR ").append(notExistsPart.toString()).append(")"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeStateDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeStateDao.java index 51e487b456..c61e58353c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeStateDao.java +++ b/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.data.repository.CrudRepository; 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.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -56,4 +57,10 @@ public class JpaRuleNodeStateDao extends JpaAbstractDao entityKeys; private PersistedAlarmRuleState state; private boolean updateFlag; - public AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, PersistedAlarmRuleState state) { + AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, Set entityKeys, PersistedAlarmRuleState state) { this.severity = severity; this.alarmRule = alarmRule; + this.entityKeys = entityKeys; if (state != null) { this.state = state; } else { @@ -76,6 +82,30 @@ public class AlarmRuleState { this.requiredRepeats = requiredRepeats; } + public boolean validateTsUpdate(Set changedKeys) { + for (EntityKey key : changedKeys) { + if (entityKeys.contains(key)) { + return true; + } + } + return false; + } + + public boolean validateAttrUpdate(Set 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) { AlarmConditionSpec spec = alarmRule.getCondition().getSpec(); 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()); switch (spec.getType()) { case SIMPLE: @@ -135,9 +165,7 @@ public class AlarmRuleState { return false; } } - long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli(); - long msFromStartOfDay = eventTs - startOfDay; - return schedule.getStartsOn() <= msFromStartOfDay && schedule.getEndsOn() > msFromStartOfDay; + return isActive(eventTs, zoneId, zdt, schedule.getStartsOn(), schedule.getEndsOn()); } private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) { @@ -147,9 +175,7 @@ public class AlarmRuleState { for (CustomTimeScheduleItem item : schedule.getItems()) { if (item.getDayOfWeek() == dayOfWeek) { if (item.isEnabled()) { - long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli(); - long msFromStartOfDay = eventTs - startOfDay; - return item.getStartsOn() <= msFromStartOfDay && item.getEndsOn() > msFromStartOfDay; + return isActive(eventTs, zoneId, zdt, item.getStartsOn(), item.getEndsOn()); } else { return false; } @@ -158,6 +184,16 @@ public class AlarmRuleState { 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() { if (state.getEventCount() > 0 || state.getLastEventTs() > 0 || state.getDuration() > 0) { 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)) { state.setEventCount(state.getEventCount() + 1); 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 (state.getLastEventTs() > 0) { 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; for (KeyFilter keyFilter : condition.getCondition()) { EntityKeyValue value = data.getValue(keyFilter.getKey()); if (value == null) { return false; } - eval = eval && eval(value, keyFilter.getPredicate()); + eval = eval && eval(data, value, keyFilter.getPredicate()); } return eval; } - private boolean eval(EntityKeyValue value, KeyFilterPredicate predicate) { + private boolean eval(DataSnapshot data, EntityKeyValue value, KeyFilterPredicate predicate) { switch (predicate.getType()) { case STRING: - return evalStrPredicate(value, (StringFilterPredicate) predicate); + return evalStrPredicate(data, value, (StringFilterPredicate) predicate); case NUMERIC: - return evalNumPredicate(value, (NumericFilterPredicate) predicate); - case COMPLEX: - return evalComplexPredicate(value, (ComplexFilterPredicate) predicate); + return evalNumPredicate(data, value, (NumericFilterPredicate) predicate); case BOOLEAN: - return evalBoolPredicate(value, (BooleanFilterPredicate) predicate); + return evalBoolPredicate(data, value, (BooleanFilterPredicate) predicate); + case COMPLEX: + return evalComplexPredicate(data, value, (ComplexFilterPredicate) predicate); default: return false; } } - private boolean evalComplexPredicate(EntityKeyValue ekv, ComplexFilterPredicate predicate) { + private boolean evalComplexPredicate(DataSnapshot data, EntityKeyValue ekv, ComplexFilterPredicate predicate) { switch (predicate.getOperation()) { case OR: for (KeyFilterPredicate kfp : predicate.getPredicates()) { - if (eval(ekv, kfp)) { + if (eval(data, ekv, kfp)) { return true; } } return false; case AND: for (KeyFilterPredicate kfp : predicate.getPredicates()) { - if (!eval(ekv, kfp)) { + if (!eval(data, ekv, kfp)) { return false; } } @@ -259,109 +295,55 @@ public class AlarmRuleState { } } - private boolean evalBoolPredicate(EntityKeyValue ekv, BooleanFilterPredicate predicate) { - Boolean value; - switch (ekv.getDataType()) { - 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) { + private boolean evalBoolPredicate(DataSnapshot data, EntityKeyValue ekv, BooleanFilterPredicate predicate) { + Boolean val = getBoolValue(ekv); + if (val == null) { return false; } + Boolean predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getBoolValue); switch (predicate.getOperation()) { case EQUAL: - return value.equals(predicate.getValue().getDefaultValue()); + return val.equals(predicateValue); case NOT_EQUAL: - return !value.equals(predicate.getValue().getDefaultValue()); + return !val.equals(predicateValue); default: throw new RuntimeException("Operation not supported: " + predicate.getOperation()); } } - private boolean evalNumPredicate(EntityKeyValue ekv, NumericFilterPredicate predicate) { - Double value; - switch (ekv.getDataType()) { - 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) { + private boolean evalNumPredicate(DataSnapshot data, EntityKeyValue ekv, NumericFilterPredicate predicate) { + Double val = getDblValue(ekv); + if (val == null) { return false; } - - Double predicateValue = predicate.getValue().getDefaultValue(); + Double predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getDblValue); switch (predicate.getOperation()) { case NOT_EQUAL: - return !value.equals(predicateValue); + return !val.equals(predicateValue); case EQUAL: - return value.equals(predicateValue); + return val.equals(predicateValue); case GREATER: - return value > predicateValue; + return val > predicateValue; case GREATER_OR_EQUAL: - return value >= predicateValue; + return val >= predicateValue; case LESS: - return value < predicateValue; + return val < predicateValue; case LESS_OR_EQUAL: - return value <= predicateValue; + return val <= predicateValue; default: throw new RuntimeException("Operation not supported: " + predicate.getOperation()); } } - private boolean evalStrPredicate(EntityKeyValue ekv, StringFilterPredicate predicate) { - String val; - String predicateValue; + private boolean evalStrPredicate(DataSnapshot data, EntityKeyValue ekv, StringFilterPredicate predicate) { + String val = getStrValue(ekv); + if (val == null) { + return false; + } + String predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getStrValue); if (predicate.isIgnoreCase()) { - val = ekv.getStrValue().toLowerCase(); - predicateValue = predicate.getValue().getDefaultValue().toLowerCase(); - } else { - val = ekv.getStrValue(); - predicateValue = predicate.getValue().getDefaultValue(); + val = val.toLowerCase(); + predicateValue = predicateValue.toLowerCase(); } switch (predicate.getOperation()) { case CONTAINS: @@ -380,4 +362,100 @@ public class AlarmRuleState { throw new RuntimeException("Operation not supported: " + predicate.getOperation()); } } + + private T getPredicateValue(DataSnapshot data, FilterPredicateValue value, Function transformFunction) { + EntityKeyValue ekv = getDynamicPredicateValue(data, value); + if (ekv != null) { + T result = transformFunction.apply(ekv); + if (result != null) { + return result; + } + } + return value.getDefaultValue(); + } + + private EntityKeyValue getDynamicPredicateValue(DataSnapshot data, FilterPredicateValue 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; + } + } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileAlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java similarity index 78% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileAlarmState.java rename to rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index f74d88fe62..5fb2c2957c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileAlarmState.java +++ b/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 lombok.Data; +import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.action.TbAlarmResult; import org.thingsboard.rule.engine.api.TbContext; 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.device.profile.DeviceProfileAlarm; 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.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceQueue; @@ -39,8 +41,10 @@ import java.util.concurrent.ExecutionException; import java.util.function.BiFunction; @Data -class DeviceProfileAlarmState { +@Slf4j +class AlarmState { + private final ProfileState deviceProfile; private final EntityId originator; private DeviceProfileAlarm alarmDefinition; private volatile List createRulesSortedBySeverityDesc; @@ -50,27 +54,33 @@ class DeviceProfileAlarmState { private volatile TbMsgMetaData lastMsgMetaData; 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.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); lastMsgMetaData = msg.getMetaData(); 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 { initCurrentAlarm(ctx); - return createOrClearAlarms(ctx, ts, AlarmRuleState::eval); + return createOrClearAlarms(ctx, ts, null, AlarmRuleState::eval); } - public boolean createOrClearAlarms(TbContext ctx, T data, BiFunction evalFunction) { + public boolean createOrClearAlarms(TbContext ctx, T data, SnapshotUpdate update, BiFunction evalFunction) { boolean stateUpdate = false; AlarmSeverity resultSeverity = null; + log.debug("[{}] processing update: {}", alarmDefinition.getId(), data); 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); stateUpdate |= state.checkUpdate(); if (evalResult) { @@ -81,9 +91,17 @@ class DeviceProfileAlarmState { if (resultSeverity != null) { pushMsg(ctx, calculateAlarmResult(ctx, resultSeverity)); } 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); if (evalResult) { 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()); pushMsg(ctx, new TbAlarmResult(false, false, true, currentAlarm)); currentAlarm = null; @@ -92,6 +110,18 @@ class DeviceProfileAlarmState { 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 { if (!initialFetchDone) { Alarm alarm = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), originator, alarmDefinition.getAlarmType()).get(); @@ -137,17 +167,20 @@ class DeviceProfileAlarmState { 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())); PersistedAlarmRuleState ruleState = alarmState == null ? null : alarmState.getClearRuleState(); 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) { 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()); AlarmSeverity oldSeverity = currentAlarm.getSeverity(); if (!oldSeverity.equals(severity)) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java index de9708dc58..a84cf96710 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java +++ b/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; -public enum AlarmStateUpdateResult { +enum AlarmStateUpdateResult { NONE, CREATED, UPDATED, SEVERITY_UPDATED, CLEARED; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceDataSnapshot.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java similarity index 70% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceDataSnapshot.java rename to rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java index f1b1067095..0c3abc0ca3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceDataSnapshot.java +++ b/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.concurrent.ConcurrentHashMap; -public class DeviceDataSnapshot { +class DataSnapshot { private volatile boolean ready; @Getter @@ -33,7 +33,7 @@ public class DeviceDataSnapshot { private final Set keys; private final Map values = new ConcurrentHashMap<>(); - public DeviceDataSnapshot(Set entityKeysToFetch) { + DataSnapshot(Set 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()) { case ATTRIBUTE: - putIfKeyExists(key, value); - putIfKeyExists(getAttrKey(key, EntityKeyType.CLIENT_ATTRIBUTE), value); - putIfKeyExists(getAttrKey(key, EntityKeyType.SHARED_ATTRIBUTE), value); - putIfKeyExists(getAttrKey(key, EntityKeyType.SERVER_ATTRIBUTE), value); + result |= putIfKeyExists(key, value, updateOfTs); + result |= putIfKeyExists(getAttrKey(key, EntityKeyType.CLIENT_ATTRIBUTE), value, updateOfTs); + result |= putIfKeyExists(getAttrKey(key, EntityKeyType.SHARED_ATTRIBUTE), value, updateOfTs); + result |= putIfKeyExists(getAttrKey(key, EntityKeyType.SERVER_ATTRIBUTE), value, updateOfTs); break; case CLIENT_ATTRIBUTE: case SHARED_ATTRIBUTE: case SERVER_ATTRIBUTE: - putIfKeyExists(key, value); - putIfKeyExists(getAttrKey(key, EntityKeyType.ATTRIBUTE), value); + result |= putIfKeyExists(key, value, updateOfTs); + result |= putIfKeyExists(getAttrKey(key, EntityKeyType.ATTRIBUTE), value, updateOfTs); break; 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)) { - values.put(key, value); + EntityKeyValue oldValue = values.put(key, value); + if (updateOfTs) { + return true; + } else { + return oldValue == null || !oldValue.equals(value); + } + } else { + return false; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileState.java deleted file mode 100644 index fd9037624e..0000000000 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileState.java +++ /dev/null @@ -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 alarmSettings = new CopyOnWriteArrayList<>(); - @Getter(AccessLevel.PACKAGE) - private final Set 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(); - } -} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java index 078be24f90..6824f56d35 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java +++ b/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; import com.google.gson.JsonParser; +import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; import org.thingsboard.rule.engine.api.TbContext; 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.DeviceProfileId; 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.KvEntry; 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.stream.Collectors; +@Slf4j class DeviceState { private final boolean persistState; private final DeviceId deviceId; + private final ProfileState deviceProfile; private RuleNodeState state; - private DeviceProfileState deviceProfile; private PersistedDeviceState pds; - private DeviceDataSnapshot latestValues; - private final ConcurrentMap alarmStates = new ConcurrentHashMap<>(); + private DataSnapshot latestValues; + private final ConcurrentMap 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.deviceId = deviceId; this.deviceProfile = deviceProfile; @@ -86,7 +87,7 @@ class DeviceState { if (pds != null) { for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { 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())) { alarmStates.get(alarm.getId()).updateState(alarm, getOrInitPersistedAlarmState(alarm)); } 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 { - for (DeviceProfileAlarmState state : alarmStates.values()) { - state.process(ctx, ts); + log.debug("[{}] Going to harvest alarms: {}", ctx.getSelfId(), 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; Alarm alarmNf = JacksonUtil.fromString(msg.getData(), Alarm.class); for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { - DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), - a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); stateChanged |= alarmState.processAlarmClear(ctx, alarmNf); } ctx.tellSuccess(msg); @@ -175,9 +182,9 @@ class DeviceState { EntityKeyType keyType = getKeyTypeFromScope(scope); keys.forEach(key -> latestValues.removeValue(new EntityKey(keyType, key))); for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { - DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), - a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); - stateChanged |= alarmState.process(ctx, msg, latestValues); + AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + stateChanged |= alarmState.process(ctx, msg, latestValues, null); } } ctx.tellSuccess(msg); @@ -192,11 +199,11 @@ class DeviceState { private boolean processAttributesUpdate(TbContext ctx, TbMsg msg, Set attributes, String scope) throws ExecutionException, InterruptedException { boolean stateChanged = false; if (!attributes.isEmpty()) { - merge(latestValues, attributes, scope); + SnapshotUpdate update = merge(latestValues, attributes, scope); for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { - DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), - a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); - stateChanged |= alarmState.process(ctx, msg, latestValues); + AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + stateChanged |= alarmState.process(ctx, msg, latestValues, update); } } ctx.tellSuccess(msg); @@ -206,34 +213,47 @@ class DeviceState { protected boolean processTelemetry(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { boolean stateChanged = false; Map> tsKvMap = JsonConverter.convertToSortedTelemetry(new JsonParser().parse(msg.getData()), TbMsgTimeseriesNode.getTs(msg)); + // iterate over data by ts (ASC order). for (Map.Entry> entry : tsKvMap.entrySet()) { Long ts = entry.getKey(); List data = entry.getValue(); - merge(latestValues, ts, data); - for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { - DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), - a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm))); - stateChanged |= alarmState.process(ctx, msg, latestValues); + SnapshotUpdate update = merge(latestValues, ts, data); + if (update.hasUpdate()) { + for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { + AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + stateChanged |= alarmState.process(ctx, msg, latestValues, update); + } } } ctx.tellSuccess(msg); return stateChanged; } - private void merge(DeviceDataSnapshot latestValues, Long ts, List data) { - latestValues.setTs(ts); + private SnapshotUpdate merge(DataSnapshot latestValues, Long newTs, List data) { + Set keys = new HashSet<>(); 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 attributes, String scope) { - long ts = latestValues.getTs(); + private SnapshotUpdate merge(DataSnapshot latestValues, Set attributes, String scope) { + long newTs = 0; + Set keys = new HashSet<>(); for (AttributeKvEntry entry : attributes) { - ts = Math.max(ts, entry.getLastUpdateTs()); - latestValues.putValue(new EntityKey(getKeyTypeFromScope(scope), entry.getKey()), toEntityValue(entry)); + newTs = Math.max(newTs, entry.getLastUpdateTs()); + 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) { @@ -248,14 +268,14 @@ class DeviceState { return EntityKeyType.ATTRIBUTE; } - private DeviceDataSnapshot fetchLatestValues(TbContext ctx, EntityId originator) throws ExecutionException, InterruptedException { + private DataSnapshot fetchLatestValues(TbContext ctx, EntityId originator) throws ExecutionException, InterruptedException { Set entityKeysToFetch = deviceProfile.getEntityKeys(); - DeviceDataSnapshot result = new DeviceDataSnapshot(entityKeysToFetch); + DataSnapshot result = new DataSnapshot(entityKeysToFetch); addEntityKeysToSnapshot(ctx, originator, entityKeysToFetch, result); return result; } - private void addEntityKeysToSnapshot(TbContext ctx, EntityId originator, Set entityKeysToFetch, DeviceDataSnapshot result) throws InterruptedException, ExecutionException { + private void addEntityKeysToSnapshot(TbContext ctx, EntityId originator, Set entityKeysToFetch, DataSnapshot result) throws InterruptedException, ExecutionException { Set serverAttributeKeys = new HashSet<>(); Set clientAttributeKeys = new HashSet<>(); Set sharedAttributeKeys = new HashSet<>(); @@ -291,16 +311,16 @@ class DeviceState { if (device != null) { switch (key) { case EntityKeyMapping.NAME: - result.putValue(entityKey, EntityKeyValue.fromString(device.getName())); + result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromString(device.getName())); break; case EntityKeyMapping.TYPE: - result.putValue(entityKey, EntityKeyValue.fromString(device.getType())); + result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromString(device.getType())); break; case EntityKeyMapping.CREATED_TIME: - result.putValue(entityKey, EntityKeyValue.fromLong(device.getCreatedTime())); + result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromLong(device.getCreatedTime())); break; case EntityKeyMapping.LABEL: - result.putValue(entityKey, EntityKeyValue.fromString(device.getLabel())); + result.putValue(entityKey, device.getCreatedTime(), EntityKeyValue.fromString(device.getLabel())); break; } } @@ -312,7 +332,7 @@ class DeviceState { List data = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), originator, latestTsKeys).get(); for (TsKvEntry entry : data) { 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 commonAttributeKeys, List data) { + private void addToSnapshot(DataSnapshot snapshot, Set commonAttributeKeys, List data) { for (AttributeKvEntry entry : data) { if (entry.getValue() != null) { 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())) { - snapshot.putValue(new EntityKey(EntityKeyType.ATTRIBUTE, entry.getKey()), value); + snapshot.putValue(new EntityKey(EntityKeyType.ATTRIBUTE, entry.getKey()), entry.getLastUpdateTs(), value); } } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java index 40ca323307..73a4db63b0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java +++ b/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; +import lombok.EqualsAndHashCode; import lombok.Getter; import org.thingsboard.server.common.data.kv.DataType; +@EqualsAndHashCode class EntityKeyValue { @Getter diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java new file mode 100644 index 0000000000..b758a96c39 --- /dev/null +++ b/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 alarmSettings = new CopyOnWriteArrayList<>(); + @Getter(AccessLevel.PACKAGE) + private final Set entityKeys = ConcurrentHashMap.newKeySet(); + + private final Map>> alarmCreateKeys = new HashMap<>(); + private final Map> 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> createAlarmKeys = alarmCreateKeys.computeIfAbsent(alarm.getId(), id -> new HashMap<>()); + alarm.getCreateRules().forEach(((severity, alarmRule) -> { + Set 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 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 entityKeys, Set 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 getCreateAlarmKeys(String id, AlarmSeverity severity) { + Map> sKeys = alarmCreateKeys.get(id); + if (sKeys == null) { + return Collections.emptySet(); + } else { + Set keys = sKeys.get(severity); + if (keys == null) { + return Collections.emptySet(); + } else { + return keys; + } + } + } + + Set getClearAlarmKeys(String id) { + Set keys = alarmClearKeys.get(id); + if (keys == null) { + return Collections.emptySet(); + } else { + return keys; + } + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/SnapshotUpdate.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/SnapshotUpdate.java new file mode 100644 index 0000000000..e52a784c9c --- /dev/null +++ b/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 keys; + + SnapshotUpdate(EntityKeyType type, Set keys) { + this.type = type; + this.keys = keys; + } + + boolean hasUpdate(){ + return !keys.isEmpty(); + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java index 54a0fa7085..4b0b87043a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java +++ b/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.TbNodeException; 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.Device; 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.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.dao.util.mapping.JacksonUtil; -import java.util.HashMap; import java.util.Map; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -70,11 +68,14 @@ public class TbDeviceProfileNode implements TbNode { this.cache = ctx.getDeviceProfileCache(); scheduleAlarmHarvesting(ctx); if (config.isFetchAlarmRulesStateOnStart()) { + log.info("[{}] Fetching alarm rule state", ctx.getSelfId()); + int fetchCount = 0; PageLink pageLink = new PageLink(1024); while (true) { PageData states = ctx.findRuleNodeStates(pageLink); if (!states.getData().isEmpty()) { for (RuleNodeState rns : states.getData()) { + fetchCount++; if (rns.getEntityId().getEntityType().equals(EntityType.DEVICE) && ctx.isLocalEntity(rns.getEntityId())) { getOrCreateDeviceState(ctx, new DeviceId(rns.getEntityId().getId()), rns); } @@ -86,6 +87,11 @@ public class TbDeviceProfileNode implements TbNode { 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)) { + log.info("[{}] Received device profile update notification: {}", ctx.getSelfId(), msg.getData()); if (msg.getType().equals("ENTITY_UPDATED")) { DeviceProfile deviceProfile = JacksonUtil.fromString(msg.getData(), DeviceProfile.class); - for (DeviceState state : deviceStates.values()) { - if (deviceProfile.getId().equals(state.getProfileId())) { - state.updateProfile(ctx, deviceProfile); + if (deviceProfile != null) { + for (DeviceState state : deviceStates.values()) { + 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 public void destroy() { deviceStates.clear(); @@ -150,7 +165,7 @@ public class TbDeviceProfileNode implements TbNode { if (deviceState == null) { DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId); 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); } } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 9bbac4154d..15d33f709c 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/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 { AlarmRuleComponent } from './profile/alarm/alarm-rule.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 { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component'; import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component'; import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.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({ declarations: @@ -190,7 +193,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen DeviceProfileTransportConfigurationComponent, CreateAlarmRulesComponent, AlarmRuleComponent, - AlarmRuleKeyFiltersDialogComponent, + AlarmRuleConditionDialogComponent, AlarmRuleConditionComponent, DeviceProfileAlarmComponent, DeviceProfileAlarmsComponent, @@ -198,9 +201,12 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen DeviceProfileDialogComponent, AddDeviceProfileDialogComponent, RuleChainAutocompleteComponent, + AlarmScheduleInfoComponent, AlarmScheduleComponent, DeviceWizardDialogComponent, - DeviceCredentialsComponent + DeviceCredentialsComponent, + AlarmScheduleDialogComponent, + EditAlarmDetailsDialogComponent ], imports: [ CommonModule, @@ -271,7 +277,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen DeviceProfileTransportConfigurationComponent, CreateAlarmRulesComponent, AlarmRuleComponent, - AlarmRuleKeyFiltersDialogComponent, + AlarmRuleConditionDialogComponent, AlarmRuleConditionComponent, DeviceProfileAlarmComponent, DeviceProfileAlarmsComponent, @@ -281,7 +287,10 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen RuleChainAutocompleteComponent, DeviceWizardDialogComponent, DeviceCredentialsComponent, - AlarmScheduleComponent + AlarmScheduleInfoComponent, + AlarmScheduleComponent, + AlarmScheduleDialogComponent, + EditAlarmDetailsDialogComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html new file mode 100644 index 0000000000..7be952c68d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html @@ -0,0 +1,124 @@ + +
+ +

{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}

+ + +
+ + +
+
+
+ + +
+ + device-profile.condition-type + + + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} + + + + {{ 'device-profile.condition-type-required' | translate }} + + +
+ + + + + {{ 'device-profile.condition-duration-value-required' | translate }} + + + {{ 'device-profile.condition-duration-value-range' | translate }} + + + {{ 'device-profile.condition-duration-value-range' | translate }} + + + {{ 'device-profile.condition-duration-value-pattern' | translate }} + + + + + + + {{ timeUnitTranslations.get(timeUnit) | translate }} + + + + {{ 'device-profile.condition-duration-time-unit-required' | translate }} + + +
+
+ + + + + {{ 'device-profile.condition-repeating-value-required' | translate }} + + + {{ 'device-profile.condition-repeating-value-range' | translate }} + + + {{ 'device-profile.condition-repeating-value-range' | translate }} + + + {{ 'device-profile.condition-repeating-value-pattern' | translate }} + + +
+
+
+
+
+
+ + +
+
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyState.java b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss similarity index 88% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyState.java rename to ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss index 08929bd2a2..4af573d021 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyState.java +++ b/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 * limitations under the License. */ -package org.thingsboard.rule.engine.profile; - -public class EntityKeyState { - - - +:host { + .row { + margin-top: 1em; + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts new file mode 100644 index 0000000000..4055560b38 --- /dev/null +++ b/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 + 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, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleConditionDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + 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); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html index 170182ded3..15d6173e0e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html @@ -15,8 +15,9 @@ limitations under the License. --> -
+
+
- + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss index 1d654bc0f4..0b1ccabd65 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss @@ -21,10 +21,15 @@ } } .tb-alarm-rule-condition { - padding: 8px; - border: 1px groove rgba(0, 0, 0, .25); - border-radius: 4px; cursor: pointer; + .tb-alarm-rule-condition-spec { + margin-top: 1em; + line-height: 1.8em; + padding: 4px; + &.disabled { + opacity: 0.7; + } + } } } diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts index 7416b944eb..23e52cf8cd 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts +++ b/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 { ControlValueAccessor, FormBuilder, - FormControl, + FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, - Validator + Validator, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; -import { KeyFilter } from '@shared/models/query/query.models'; -import { deepClone } from '@core/utils'; -import { - AlarmRuleKeyFiltersDialogComponent, - AlarmRuleKeyFiltersDialogData -} from './alarm-rule-key-filters-dialog.component'; +import { deepClone, isUndefined } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; 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({ selector: 'tb-alarm-rule-condition', @@ -55,9 +56,11 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit @Input() disabled: boolean; - alarmRuleConditionControl: FormControl; + alarmRuleConditionFormGroup: FormGroup; + + specText = ''; - private modelValue: Array; + private modelValue: AlarmCondition; private propagateChange = (v: any) => { }; @@ -75,25 +78,31 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit } ngOnInit() { - this.alarmRuleConditionControl = this.fb.control(null); + this.alarmRuleConditionFormGroup = this.fb.group({ + condition: [null, Validators.required], + spec: [null, Validators.required] + }); } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { - this.alarmRuleConditionControl.disable({emitEvent: false}); + this.alarmRuleConditionFormGroup.disable({emitEvent: false}); } else { - this.alarmRuleConditionControl.enable({emitEvent: false}); + this.alarmRuleConditionFormGroup.enable({emitEvent: false}); } } - writeValue(value: Array): void { + writeValue(value: AlarmCondition): void { this.modelValue = value; + if (this.modelValue !== null && isUndefined(this.modelValue?.spec)) { + this.modelValue = Object.assign(this.modelValue, {spec: {type: AlarmConditionType.SIMPLE}}); + } this.updateConditionInfo(); } public conditionSet() { - return this.modelValue && this.modelValue.length; + return this.modelValue && this.modelValue.condition.length; } public validate(c: FormControl) { @@ -108,13 +117,13 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit if ($event) { $event.stopPropagation(); } - this.dialog.open>(AlarmRuleKeyFiltersDialogComponent, { + this.dialog.open(AlarmRuleConditionDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { readonly: this.disabled, - keyFilters: this.disabled ? this.modelValue : deepClone(this.modelValue) + condition: this.disabled ? this.modelValue : deepClone(this.modelValue) } }).afterClosed().subscribe((result) => { if (result) { @@ -125,7 +134,45 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit } 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() { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html index 66eb99f27e..3c49f61cf0 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html @@ -16,90 +16,36 @@ -->
- - - - -
- - device-profile.condition-type - - - {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} - - - - {{ 'device-profile.condition-type-required' | translate }} - - -
- - - - - {{ 'device-profile.condition-duration-value-required' | translate }} - - - {{ 'device-profile.condition-duration-value-range' | translate }} - - - {{ 'device-profile.condition-duration-value-range' | translate }} - - - {{ 'device-profile.condition-duration-value-pattern' | translate }} - - - - - - - {{ timeUnitTranslations.get(timeUnit) | translate }} - - - - {{ 'device-profile.condition-duration-time-unit-required' | translate }} - - -
-
- - - - - {{ 'device-profile.condition-repeating-value-required' | translate }} - - - {{ 'device-profile.condition-repeating-value-range' | translate }} - - - {{ 'device-profile.condition-repeating-value-range' | translate }} - - - {{ 'device-profile.condition-repeating-value-pattern' | translate }} - - -
-
-
- - - - - - - device-profile.alarm-details - - - -
+ + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss index 8af986af69..a52e6f312e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss @@ -17,5 +17,29 @@ .row { 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; + } + } + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts index fe2916b854..4c86f7dfbd 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts @@ -25,11 +25,14 @@ import { Validator, Validators } 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 { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; 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({ selector: 'tb-alarm-rule', @@ -50,12 +53,6 @@ import { isUndefined } from '@core/utils'; }) export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { - timeUnits = Object.keys(TimeUnit); - timeUnitTranslations = timeUnitTranslationMap; - alarmConditionTypes = Object.keys(AlarmConditionType); - AlarmConditionType = AlarmConditionType; - alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap; - @Input() disabled: boolean; @@ -72,6 +69,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat alarmRuleFormGroup: FormGroup; + expandAlarmDetails = false; + private propagateChange = (v: any) => { }; constructor(private dialog: MatDialog, @@ -87,21 +86,10 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat ngOnInit() { this.alarmRuleFormGroup = this.fb.group({ - condition: this.fb.group({ - 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), + condition: [null, [Validators.required]], schedule: [null], alarmDetails: [null] }); - this.alarmRuleFormGroup.get('condition.spec.type').valueChanges.subscribe((type) => { - this.updateValidators(type, true, true); - }); this.alarmRuleFormGroup.valueChanges.subscribe(() => { this.updateModel(); }); @@ -118,11 +106,25 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat writeValue(value: AlarmRule): void { 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.updateValidators(this.modelValue?.condition?.spec?.type); + } + + public openEditDetailsDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(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) { @@ -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() { const value = this.alarmRuleFormGroup.value; if (this.modelValue) { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html similarity index 74% rename from ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html rename to ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html index 26defe62ff..abe511fc3f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html @@ -15,9 +15,9 @@ limitations under the License. --> -
+ -

{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}

+

{{ (readonly ? 'device-profile.schedule' : 'device-profile.edit-schedule') | translate }}

@@ -43,7 +40,7 @@ + + + +
+
+
+ + device-profile.alarm-details + + +
+
+
+
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts new file mode 100644 index 0000000000..bc46bee51d --- /dev/null +++ b/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 + implements OnInit, ErrorStateMatcher { + + alarmDetails = this.data.alarmDetails; + + editDetailsFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: EditAlarmDetailsDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + 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); + } +} diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 9e861c1359..bbbde588b7 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -266,11 +266,11 @@ export class DeviceWizardDialogComponent extends }) ); } else { - return of(null); + return of(this.deviceWizardFormGroup.get('deviceProfileId').value); } } - private createDevice(profileId: EntityId = this.deviceWizardFormGroup.get('deviceProfileId').value): Observable> { + private createDevice(profileId): Observable> { const device = { name: this.deviceWizardFormGroup.get('name').value, label: this.deviceWizardFormGroup.get('label').value, diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html index 437fc21c86..76dfe27511 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html @@ -61,29 +61,7 @@ - - - - - - - - - - - - - + + diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index d1fb40a9d5..4354521330 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/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 { KeyFilter } from '@shared/models/query/query.models'; import { TimeUnit } from '@shared/models/time/time.models'; +import * as _moment from 'moment-timezone'; +import { AbstractControl, FormGroup } from '@angular/forms'; export enum DeviceProfileType { DEFAULT = 'DEFAULT' @@ -408,3 +410,62 @@ export interface ClaimResult { device: Device; response: ClaimResponse; } + +export const dayOfWeekTranslations = new Array( + '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 `${start.format('hh:mm A')} – ${end.format('hh:mm A')}`; + } else if (start.valueOf() === 0 && end.valueOf() === 0 || start.isSame(_moment([1970, 0])) && end.isSame(_moment([1970, 0]))) { + return '12:00 AM – 12:00 PM'; + } + return `12:00 AM – ${end.format('hh:mm A')}` + + ` and ${start.format('hh:mm A')} – 12:00 PM`; +} diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index 3f4b6896db..ba86bce85b 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -476,21 +476,23 @@ export function keyFilterInfosToKeyFilters(keyFilterInfos: Array) export function keyFiltersToKeyFilterInfos(keyFilters: Array): Array { const keyFilterInfos: Array = []; const keyFilterInfoMap: {[infoKey: string]: KeyFilterInfo} = {}; - for (const keyFilter of keyFilters) { - const key = keyFilter.key; - const infoKey = key.key + key.type + keyFilter.valueType; - let keyFilterInfo = keyFilterInfoMap[infoKey]; - if (!keyFilterInfo) { - keyFilterInfo = { - key, - valueType: keyFilter.valueType, - predicates: [] - }; - keyFilterInfoMap[infoKey] = keyFilterInfo; - keyFilterInfos.push(keyFilterInfo); - } - if (keyFilter.predicate) { - keyFilterInfo.predicates.push(keyFilterPredicateToKeyFilterPredicateInfo(keyFilter.predicate)); + if (keyFilters) { + for (const keyFilter of keyFilters) { + const key = keyFilter.key; + const infoKey = key.key + key.type + keyFilter.valueType; + let keyFilterInfo = keyFilterInfoMap[infoKey]; + if (!keyFilterInfo) { + keyFilterInfo = { + key, + valueType: keyFilter.valueType, + predicates: [] + }; + keyFilterInfoMap[infoKey] = keyFilterInfo; + keyFilterInfos.push(keyFilterInfo); + } + if (keyFilter.predicate) { + keyFilterInfo.predicates.push(keyFilterPredicateToKeyFilterPredicateInfo(keyFilter.predicate)); + } } } return keyFilterInfos; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 8f89fa8f39..9a5d71a2e6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -55,7 +55,9 @@ "continue": "Continue", "discard-changes": "Discard Changes", "download": "Download", - "next-with-label": "Next: {{label}}" + "next-with-label": "Next: {{label}}", + "read-more": "Read more", + "hide": "Hide" }, "aggregation": { "aggregation": "Aggregation", @@ -932,15 +934,18 @@ "condition-type": "Condition type", "condition-type-simple": "Simple", "condition-type-duration": "Duration", + "condition-during": "During {{during}}", "condition-type-repeating": "Repeating", "condition-type-required": "Condition type is required.", "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-pattern": "Count of events should be integers.", "condition-repeating-value-required": "Count of events is required.", + "condition-repeat-times": "Repeats { count, plural, 1 {1 time} other {# times} }", "schedule-type": "Scheduler type", "schedule-type-required": "Scheduler type is required.", "schedule": "Schedule", + "edit-schedule": "Edit alarm schedule", "schedule-any-time": "Active all the time", "schedule-specific-time": "Active at a specific time", "schedule-custom": "Custom", @@ -956,7 +961,8 @@ "schedule-days": "Days", "schedule-time": "Time", "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": { "close": "Close dialog"