Commit 1c2ec66f16cc50e1d352612f7e94f09adca85709

Authored by Vladyslav_Prykhodko
2 parents 1f762335 d0fafb95

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

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