Browse Source

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

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

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

@ -469,6 +469,14 @@ class DefaultTbContext implements TbContext {
return mainCtx.getRuleNodeStateService().save(getTenantId(), state);
}
@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());

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

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

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

@ -42,6 +42,7 @@ import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.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<OAuth2ClientRegistrationTemplate> 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());

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

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

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

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

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

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

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

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

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

@ -29,6 +29,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import 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<OAuth2ClientRegistrationTemplate> 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);

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

@ -68,6 +68,17 @@ public class BaseRuleNodeStateService extends AbstractEntityService implements R
return saveOrUpdate(tenantId, ruleNodeState, false);
}
@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) {

4
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<RuleNodeState> {
PageData<RuleNodeState> findByRuleNodeId(UUID ruleNodeId, PageLink pageLink);
RuleNodeState findByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId);
void removeByRuleNodeId(UUID ruleNodeId);
}

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

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

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

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

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

@ -472,13 +472,13 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
if (entityFilter.isFetchLastLevelOnly()) {
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(")");

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

@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.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<RuleNodeStateEntity, Rul
public RuleNodeState findByRuleNodeIdAndEntityId(UUID ruleNodeId, UUID entityId) {
return DaoUtil.getData(ruleNodeStateRepository.findByRuleNodeIdAndEntityId(ruleNodeId, entityId));
}
@Transactional
@Override
public void removeByRuleNodeId(UUID ruleNodeId) {
ruleNodeStateRepository.removeByRuleNodeId(ruleNodeId);
}
}

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

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

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

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

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

@ -29,6 +29,9 @@ import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpe
import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.KeyFilterPredicate;
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
@ -38,22 +41,25 @@ import org.thingsboard.server.common.msg.tools.SchedulerUtils;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Set;
import java.util.function.Function;
@Data
public class AlarmRuleState {
class AlarmRuleState {
private final AlarmSeverity severity;
private final AlarmRule alarmRule;
private final AlarmConditionSpec spec;
private final long requiredDurationInMs;
private final long requiredRepeats;
private final Set<EntityKey> entityKeys;
private PersistedAlarmRuleState state;
private boolean updateFlag;
public AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, PersistedAlarmRuleState state) {
AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, Set<EntityKey> entityKeys, PersistedAlarmRuleState state) {
this.severity = severity;
this.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<EntityKey> changedKeys) {
for (EntityKey key : changedKeys) {
if (entityKeys.contains(key)) {
return true;
}
}
return false;
}
public boolean validateAttrUpdate(Set<EntityKey> changedKeys) {
//If the attribute was updated, but no new telemetry arrived - we ignore this until new telemetry is there.
for (EntityKey key : entityKeys) {
if (key.getType().equals(EntityKeyType.TIME_SERIES)) {
return false;
}
}
for (EntityKey key : changedKeys) {
if (entityKeys.contains(key)) {
return true;
}
}
return false;
}
public AlarmConditionSpec getSpec(AlarmRule alarmRule) {
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> T getPredicateValue(DataSnapshot data, FilterPredicateValue<T> value, Function<EntityKeyValue, T> transformFunction) {
EntityKeyValue ekv = getDynamicPredicateValue(data, value);
if (ekv != null) {
T result = transformFunction.apply(ekv);
if (result != null) {
return result;
}
}
return value.getDefaultValue();
}
private <T> EntityKeyValue getDynamicPredicateValue(DataSnapshot data, FilterPredicateValue<T> value) {
EntityKeyValue ekv = null;
if (value.getDynamicValue() != null) {
ekv = data.getValue(new EntityKey(EntityKeyType.ATTRIBUTE, value.getDynamicValue().getSourceAttribute()));
if (ekv == null) {
ekv = data.getValue(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, value.getDynamicValue().getSourceAttribute()));
if (ekv == null) {
ekv = data.getValue(new EntityKey(EntityKeyType.SHARED_ATTRIBUTE, value.getDynamicValue().getSourceAttribute()));
if (ekv == null) {
ekv = data.getValue(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, value.getDynamicValue().getSourceAttribute()));
}
}
}
}
return ekv;
}
private static String getStrValue(EntityKeyValue ekv) {
switch (ekv.getDataType()) {
case LONG:
return ekv.getLngValue() != null ? ekv.getLngValue().toString() : null;
case DOUBLE:
return ekv.getDblValue() != null ? ekv.getDblValue().toString() : null;
case BOOLEAN:
return ekv.getBoolValue() != null ? ekv.getBoolValue().toString() : null;
case STRING:
return ekv.getStrValue();
case JSON:
return ekv.getJsonValue();
default:
return null;
}
}
private static Double getDblValue(EntityKeyValue ekv) {
switch (ekv.getDataType()) {
case LONG:
return ekv.getLngValue() != null ? ekv.getLngValue().doubleValue() : null;
case DOUBLE:
return ekv.getDblValue() != null ? ekv.getDblValue() : null;
case BOOLEAN:
return ekv.getBoolValue() != null ? (ekv.getBoolValue() ? 1.0 : 0.0) : null;
case STRING:
try {
return Double.parseDouble(ekv.getStrValue());
} catch (RuntimeException e) {
return null;
}
case JSON:
try {
return Double.parseDouble(ekv.getJsonValue());
} catch (RuntimeException e) {
return null;
}
default:
return null;
}
}
private static Boolean getBoolValue(EntityKeyValue ekv) {
switch (ekv.getDataType()) {
case LONG:
return ekv.getLngValue() != null ? ekv.getLngValue() > 0 : null;
case DOUBLE:
return ekv.getDblValue() != null ? ekv.getDblValue() > 0 : null;
case BOOLEAN:
return ekv.getBoolValue();
case STRING:
try {
return Boolean.parseBoolean(ekv.getStrValue());
} catch (RuntimeException e) {
return null;
}
case JSON:
try {
return Boolean.parseBoolean(ekv.getJsonValue());
} catch (RuntimeException e) {
return null;
}
default:
return null;
}
}
}

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

@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.profile;
import com.fasterxml.jackson.databind.JsonNode;
import 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<AlarmRuleState> 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 <T> boolean createOrClearAlarms(TbContext ctx, T data, BiFunction<AlarmRuleState, T, Boolean> evalFunction) {
public <T> boolean createOrClearAlarms(TbContext ctx, T data, SnapshotUpdate update, BiFunction<AlarmRuleState, T, Boolean> evalFunction) {
boolean stateUpdate = false;
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)) {

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

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

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

@ -24,7 +24,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class DeviceDataSnapshot {
class DataSnapshot {
private volatile boolean ready;
@Getter
@ -33,7 +33,7 @@ public class DeviceDataSnapshot {
private final Set<EntityKey> keys;
private final Map<EntityKey, EntityKeyValue> values = new ConcurrentHashMap<>();
public DeviceDataSnapshot(Set<EntityKey> entityKeysToFetch) {
DataSnapshot(Set<EntityKey> 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;
}
}

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

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

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

@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.profile;
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<String, DeviceProfileAlarmState> alarmStates = new ConcurrentHashMap<>();
private DataSnapshot latestValues;
private final ConcurrentMap<String, AlarmState> alarmStates = new ConcurrentHashMap<>();
public DeviceState(TbContext ctx, TbDeviceProfileNodeConfiguration config, DeviceId deviceId, DeviceProfileState deviceProfile, RuleNodeState state) {
DeviceState(TbContext ctx, TbDeviceProfileNodeConfiguration config, DeviceId deviceId, ProfileState deviceProfile, RuleNodeState state) {
this.persistState = config.isPersistAlarmRulesState();
this.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<AttributeKvEntry> 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<Long, List<KvEntry>> tsKvMap = JsonConverter.convertToSortedTelemetry(new JsonParser().parse(msg.getData()), TbMsgTimeseriesNode.getTs(msg));
// iterate over data by ts (ASC order).
for (Map.Entry<Long, List<KvEntry>> entry : tsKvMap.entrySet()) {
Long ts = entry.getKey();
List<KvEntry> 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<KvEntry> data) {
latestValues.setTs(ts);
private SnapshotUpdate merge(DataSnapshot latestValues, Long newTs, List<KvEntry> data) {
Set<EntityKey> 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<AttributeKvEntry> attributes, String scope) {
long ts = latestValues.getTs();
private SnapshotUpdate merge(DataSnapshot latestValues, Set<AttributeKvEntry> attributes, String scope) {
long newTs = 0;
Set<EntityKey> 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<EntityKey> 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<EntityKey> entityKeysToFetch, DeviceDataSnapshot result) throws InterruptedException, ExecutionException {
private void addEntityKeysToSnapshot(TbContext ctx, EntityId originator, Set<EntityKey> entityKeysToFetch, DataSnapshot result) throws InterruptedException, ExecutionException {
Set<String> serverAttributeKeys = new HashSet<>();
Set<String> clientAttributeKeys = new HashSet<>();
Set<String> 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<TsKvEntry> 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<String> commonAttributeKeys, List<AttributeKvEntry> data) {
private void addToSnapshot(DataSnapshot snapshot, Set<String> commonAttributeKeys, List<AttributeKvEntry> 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);
}
}
}

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

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

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

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

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

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

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

@ -23,7 +23,6 @@ import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.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<RuleNodeState> 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);
}
}

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

@ -102,13 +102,16 @@ import { DeviceProfileAlarmComponent } from './profile/alarm/device-profile-alar
import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component';
import { 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,

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

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

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

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

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

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

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

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

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

@ -21,10 +21,15 @@
}
}
.tb-alarm-rule-condition {
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;
}
}
}
}

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

@ -18,20 +18,21 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core';
import {
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<KeyFilter>;
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<KeyFilter>): 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, AlarmRuleKeyFiltersDialogData,
Array<KeyFilter>>(AlarmRuleKeyFiltersDialogComponent, {
this.dialog.open<AlarmRuleConditionDialogComponent, AlarmRuleConditionDialogData,
AlarmCondition>(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() {

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

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

24
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;
}
}
}
}

93
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, EditAlarmDetailsDialogData,
string>(EditAlarmDetailsDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value
}
}).afterClosed().subscribe((alarmDetails) => {
if (isDefinedAndNotNull(alarmDetails)) {
this.alarmRuleFormGroup.patchValue({alarmDetails});
}
});
}
public validate(c: FormControl) {
@ -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) {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -25,6 +25,8 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { EntityInfoData } from '@shared/models/entity.models';
import { 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<string>(
'device-profile.schedule-day.monday',
'device-profile.schedule-day.tuesday',
'device-profile.schedule-day.wednesday',
'device-profile.schedule-day.thursday',
'device-profile.schedule-day.friday',
'device-profile.schedule-day.saturday',
'device-profile.schedule-day.sunday'
);
export function getDayString(day: number): string {
switch (day) {
case 0:
return 'device-profile.schedule-day.monday';
case 1:
return this.translate.instant('device-profile.schedule-day.tuesday');
case 2:
return this.translate.instant('device-profile.schedule-day.wednesday');
case 3:
return this.translate.instant('device-profile.schedule-day.thursday');
case 4:
return this.translate.instant('device-profile.schedule-day.friday');
case 5:
return this.translate.instant('device-profile.schedule-day.saturday');
case 6:
return this.translate.instant('device-profile.schedule-day.sunday');
}
}
export function timeOfDayToUTCTimestamp(date: Date | number): number {
if (typeof date === 'number' || date === null) {
return 0;
}
return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf();
}
export function utcTimestampToTimeOfDay(time = 0): Date {
return new Date(time + new Date().getTimezoneOffset() * 60 * 1000);
}
function timeOfDayToMoment(date: Date | number): _moment.Moment {
if (typeof date === 'number' || date === null) {
return _moment([1970, 0, 1, 0, 0, 0, 0]);
}
return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 0, 0]);
}
export function getAlarmScheduleRangeText(startsOn: Date | number, endsOn: Date | number): string {
const start = timeOfDayToMoment(startsOn);
const end = timeOfDayToMoment(endsOn);
if (start < end) {
return `<span><span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">${end.format('hh:mm A')}</span></span>`;
} else if (start.valueOf() === 0 && end.valueOf() === 0 || start.isSame(_moment([1970, 0])) && end.isSame(_moment([1970, 0]))) {
return '<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">12:00 PM</span></span>';
}
return `<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">${end.format('hh:mm A')}</span>` +
` and <span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">12:00 PM</span></span>`;
}

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

@ -476,21 +476,23 @@ export function keyFilterInfosToKeyFilters(keyFilterInfos: Array<KeyFilterInfo>)
export function keyFiltersToKeyFilterInfos(keyFilters: Array<KeyFilter>): Array<KeyFilterInfo> {
const keyFilterInfos: Array<KeyFilterInfo> = [];
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;

10
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 <b>{{during}}</b>",
"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 <b>{ count, plural, 1 {1 time} other {# times} }</b>",
"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"

Loading…
Cancel
Save