Commit 1c2ec66f16cc50e1d352612f7e94f09adca85709
Merge remote-tracking branch 'upstream/master' into improvement/oauth2
Showing
53 changed files
with
1503 additions
and
649 deletions
... | ... | @@ -469,6 +469,14 @@ class DefaultTbContext implements TbContext { |
469 | 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(); | ... | ... |
... | ... | @@ -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 | } | ... | ... |
... | ... | @@ -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)) { | ... | ... |
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, | ... | ... |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html
0 → 100644
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2020 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<form [formGroup]="conditionFormGroup" (ngSubmit)="save()" style="width: 700px;"> | |
19 | + <mat-toolbar color="primary"> | |
20 | + <h2>{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}</h2> | |
21 | + <span fxFlex></span> | |
22 | + <button mat-icon-button | |
23 | + (click)="cancel()" | |
24 | + type="button"> | |
25 | + <mat-icon class="material-icons">close</mat-icon> | |
26 | + </button> | |
27 | + </mat-toolbar> | |
28 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | |
29 | + </mat-progress-bar> | |
30 | + <div mat-dialog-content> | |
31 | + <fieldset [disabled]="isLoading$ | async"> | |
32 | + <div fxFlex fxLayout="column"> | |
33 | + <tb-key-filter-list | |
34 | + [displayUserParameters]="false" | |
35 | + [allowUserDynamicSource]="false" | |
36 | + [telemetryKeysOnly]="true" | |
37 | + formControlName="keyFilters"> | |
38 | + </tb-key-filter-list> | |
39 | + <section formGroupName="spec" class="row"> | |
40 | + <mat-form-field class="mat-block" hideRequiredMarker> | |
41 | + <mat-label translate>device-profile.condition-type</mat-label> | |
42 | + <mat-select formControlName="type" required> | |
43 | + <mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType"> | |
44 | + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} | |
45 | + </mat-option> | |
46 | + </mat-select> | |
47 | + <mat-error *ngIf="conditionFormGroup.get('spec.type').hasError('required')"> | |
48 | + {{ 'device-profile.condition-type-required' | translate }} | |
49 | + </mat-error> | |
50 | + </mat-form-field> | |
51 | + <div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.DURATION"> | |
52 | + <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always"> | |
53 | + <mat-label></mat-label> | |
54 | + <input type="number" required | |
55 | + step="1" min="1" max="2147483647" matInput | |
56 | + placeholder="{{ 'device-profile.condition-duration-value' | translate }}" | |
57 | + formControlName="value"> | |
58 | + <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('required')"> | |
59 | + {{ 'device-profile.condition-duration-value-required' | translate }} | |
60 | + </mat-error> | |
61 | + <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('min')"> | |
62 | + {{ 'device-profile.condition-duration-value-range' | translate }} | |
63 | + </mat-error> | |
64 | + <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('max')"> | |
65 | + {{ 'device-profile.condition-duration-value-range' | translate }} | |
66 | + </mat-error> | |
67 | + <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('pattern')"> | |
68 | + {{ 'device-profile.condition-duration-value-pattern' | translate }} | |
69 | + </mat-error> | |
70 | + </mat-form-field> | |
71 | + <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always"> | |
72 | + <mat-label></mat-label> | |
73 | + <mat-select formControlName="unit" | |
74 | + required | |
75 | + placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}"> | |
76 | + <mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit"> | |
77 | + {{ timeUnitTranslations.get(timeUnit) | translate }} | |
78 | + </mat-option> | |
79 | + </mat-select> | |
80 | + <mat-error *ngIf="conditionFormGroup.get('spec.unit').hasError('required')"> | |
81 | + {{ 'device-profile.condition-duration-time-unit-required' | translate }} | |
82 | + </mat-error> | |
83 | + </mat-form-field> | |
84 | + </div> | |
85 | + <div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.REPEATING"> | |
86 | + <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always"> | |
87 | + <mat-label></mat-label> | |
88 | + <input type="number" required | |
89 | + step="1" min="1" max="2147483647" matInput | |
90 | + placeholder="{{ 'device-profile.condition-repeating-value' | translate }}" | |
91 | + formControlName="count"> | |
92 | + <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('required')"> | |
93 | + {{ 'device-profile.condition-repeating-value-required' | translate }} | |
94 | + </mat-error> | |
95 | + <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('min')"> | |
96 | + {{ 'device-profile.condition-repeating-value-range' | translate }} | |
97 | + </mat-error> | |
98 | + <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('max')"> | |
99 | + {{ 'device-profile.condition-repeating-value-range' | translate }} | |
100 | + </mat-error> | |
101 | + <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('pattern')"> | |
102 | + {{ 'device-profile.condition-repeating-value-pattern' | translate }} | |
103 | + </mat-error> | |
104 | + </mat-form-field> | |
105 | + </div> | |
106 | + </section> | |
107 | + </div> | |
108 | + </fieldset> | |
109 | + </div> | |
110 | + <div mat-dialog-actions fxLayoutAlign="end center"> | |
111 | + <button mat-raised-button color="primary" | |
112 | + *ngIf="!readonly" | |
113 | + type="submit" | |
114 | + [disabled]="(isLoading$ | async) || conditionFormGroup.invalid || !conditionFormGroup.dirty"> | |
115 | + {{ 'action.save' | translate }} | |
116 | + </button> | |
117 | + <button mat-button color="primary" | |
118 | + type="button" | |
119 | + [disabled]="(isLoading$ | async)" | |
120 | + (click)="cancel()" cdkFocusInitial> | |
121 | + {{ (readonly ? 'action.close' : 'action.cancel') | translate }} | |
122 | + </button> | |
123 | + </div> | |
124 | +</form> | ... | ... |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss
renamed from
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyState.java
... | ... | @@ -13,10 +13,8 @@ |
13 | 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 | } | ... | ... |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts
0 → 100644
1 | +/// | |
2 | +/// Copyright © 2016-2020 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; | |
18 | +import { ErrorStateMatcher } from '@angular/material/core'; | |
19 | +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; | |
20 | +import { Store } from '@ngrx/store'; | |
21 | +import { AppState } from '@core/core.state'; | |
22 | +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; | |
23 | +import { Router } from '@angular/router'; | |
24 | +import { DialogComponent } from '@app/shared/components/dialog.component'; | |
25 | +import { UtilsService } from '@core/services/utils.service'; | |
26 | +import { TranslateService } from '@ngx-translate/core'; | |
27 | +import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models'; | |
28 | +import { AlarmCondition, AlarmConditionType, AlarmConditionTypeTranslationMap } from '@shared/models/device.models'; | |
29 | +import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; | |
30 | + | |
31 | +export interface AlarmRuleConditionDialogData { | |
32 | + readonly: boolean; | |
33 | + condition: AlarmCondition; | |
34 | +} | |
35 | + | |
36 | +@Component({ | |
37 | + selector: 'tb-alarm-rule-condition-dialog', | |
38 | + templateUrl: './alarm-rule-condition-dialog.component.html', | |
39 | + providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleConditionDialogComponent}], | |
40 | + styleUrls: ['/alarm-rule-condition-dialog.component.scss'] | |
41 | +}) | |
42 | +export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRuleConditionDialogComponent, AlarmCondition> | |
43 | + implements OnInit, ErrorStateMatcher { | |
44 | + | |
45 | + timeUnits = Object.keys(TimeUnit); | |
46 | + timeUnitTranslations = timeUnitTranslationMap; | |
47 | + alarmConditionTypes = Object.keys(AlarmConditionType); | |
48 | + AlarmConditionType = AlarmConditionType; | |
49 | + alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap; | |
50 | + | |
51 | + readonly = this.data.readonly; | |
52 | + condition = this.data.condition; | |
53 | + | |
54 | + conditionFormGroup: FormGroup; | |
55 | + | |
56 | + submitted = false; | |
57 | + | |
58 | + constructor(protected store: Store<AppState>, | |
59 | + protected router: Router, | |
60 | + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleConditionDialogData, | |
61 | + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | |
62 | + public dialogRef: MatDialogRef<AlarmRuleConditionDialogComponent, AlarmCondition>, | |
63 | + private fb: FormBuilder, | |
64 | + private utils: UtilsService, | |
65 | + public translate: TranslateService) { | |
66 | + super(store, router, dialogRef); | |
67 | + | |
68 | + this.conditionFormGroup = this.fb.group({ | |
69 | + keyFilters: [keyFiltersToKeyFilterInfos(this.condition?.condition), Validators.required], | |
70 | + spec: this.fb.group({ | |
71 | + type: [AlarmConditionType.SIMPLE, Validators.required], | |
72 | + unit: [{value: null, disable: true}, Validators.required], | |
73 | + value: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]], | |
74 | + count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]] | |
75 | + }) | |
76 | + }); | |
77 | + this.conditionFormGroup.patchValue({spec: this.condition?.spec}); | |
78 | + this.conditionFormGroup.get('spec.type').valueChanges.subscribe((type) => { | |
79 | + this.updateValidators(type, true, true); | |
80 | + }); | |
81 | + if (this.readonly) { | |
82 | + this.conditionFormGroup.disable({emitEvent: false}); | |
83 | + } else { | |
84 | + this.updateValidators(this.condition?.spec?.type); | |
85 | + } | |
86 | + } | |
87 | + | |
88 | + ngOnInit(): void { | |
89 | + } | |
90 | + | |
91 | + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | |
92 | + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); | |
93 | + const customErrorState = !!(control && control.invalid && this.submitted); | |
94 | + return originalErrorState || customErrorState; | |
95 | + } | |
96 | + | |
97 | + private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) { | |
98 | + switch (type) { | |
99 | + case AlarmConditionType.DURATION: | |
100 | + this.conditionFormGroup.get('spec.value').enable(); | |
101 | + this.conditionFormGroup.get('spec.unit').enable(); | |
102 | + this.conditionFormGroup.get('spec.count').disable(); | |
103 | + if (resetDuration) { | |
104 | + this.conditionFormGroup.get('spec').patchValue({ | |
105 | + count: null | |
106 | + }); | |
107 | + } | |
108 | + break; | |
109 | + case AlarmConditionType.REPEATING: | |
110 | + this.conditionFormGroup.get('spec.count').enable(); | |
111 | + this.conditionFormGroup.get('spec.value').disable(); | |
112 | + this.conditionFormGroup.get('spec.unit').disable(); | |
113 | + if (resetDuration) { | |
114 | + this.conditionFormGroup.get('spec').patchValue({ | |
115 | + value: null, | |
116 | + unit: null | |
117 | + }); | |
118 | + } | |
119 | + break; | |
120 | + case AlarmConditionType.SIMPLE: | |
121 | + this.conditionFormGroup.get('spec.value').disable(); | |
122 | + this.conditionFormGroup.get('spec.unit').disable(); | |
123 | + this.conditionFormGroup.get('spec.count').disable(); | |
124 | + if (resetDuration) { | |
125 | + this.conditionFormGroup.get('spec').patchValue({ | |
126 | + value: null, | |
127 | + unit: null, | |
128 | + count: null | |
129 | + }); | |
130 | + } | |
131 | + break; | |
132 | + } | |
133 | + this.conditionFormGroup.get('spec.value').updateValueAndValidity({emitEvent}); | |
134 | + this.conditionFormGroup.get('spec.unit').updateValueAndValidity({emitEvent}); | |
135 | + this.conditionFormGroup.get('spec.count').updateValueAndValidity({emitEvent}); | |
136 | + } | |
137 | + | |
138 | + cancel(): void { | |
139 | + this.dialogRef.close(null); | |
140 | + } | |
141 | + | |
142 | + save(): void { | |
143 | + this.submitted = true; | |
144 | + this.condition = { | |
145 | + condition: keyFilterInfosToKeyFilters(this.conditionFormGroup.get('keyFilters').value), | |
146 | + spec: this.conditionFormGroup.get('spec').value | |
147 | + }; | |
148 | + this.dialogRef.close(this.condition); | |
149 | + } | |
150 | +} | ... | ... |
... | ... | @@ -15,8 +15,9 @@ |
15 | 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 { | ... | ... |
... | ... | @@ -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"> | ... | ... |
... | ... | @@ -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" | ... | ... |
ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html
0 → 100644
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2020 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<form [formGroup]="editDetailsFormGroup" (ngSubmit)="save()" style="width: 800px;"> | |
19 | + <mat-toolbar color="primary"> | |
20 | + <h2>{{ 'device-profile.alarm-rule-details' | translate }}</h2> | |
21 | + <span fxFlex></span> | |
22 | + <button mat-icon-button | |
23 | + (click)="cancel()" | |
24 | + type="button"> | |
25 | + <mat-icon class="material-icons">close</mat-icon> | |
26 | + </button> | |
27 | + </mat-toolbar> | |
28 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | |
29 | + </mat-progress-bar> | |
30 | + <div mat-dialog-content> | |
31 | + <fieldset [disabled]="isLoading$ | async"> | |
32 | + <div fxFlex fxLayout="column"> | |
33 | + <mat-form-field class="mat-block"> | |
34 | + <mat-label translate>device-profile.alarm-details</mat-label> | |
35 | + <textarea matInput formControlName="alarmDetails" rows="5"></textarea> | |
36 | + </mat-form-field> | |
37 | + </div> | |
38 | + </fieldset> | |
39 | + </div> | |
40 | + <div mat-dialog-actions fxLayoutAlign="end center"> | |
41 | + <button mat-raised-button color="primary" | |
42 | + type="submit" | |
43 | + [disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty"> | |
44 | + {{ 'action.save' | translate }} | |
45 | + </button> | |
46 | + <button mat-button color="primary" | |
47 | + type="button" | |
48 | + [disabled]="(isLoading$ | async)" | |
49 | + (click)="cancel()" cdkFocusInitial> | |
50 | + {{ 'action.cancel' | translate }} | |
51 | + </button> | |
52 | + </div> | |
53 | +</form> | ... | ... |
ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts
0 → 100644
1 | +/// | |
2 | +/// Copyright © 2016-2020 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; | |
18 | +import { ErrorStateMatcher } from '@angular/material/core'; | |
19 | +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; | |
20 | +import { Store } from '@ngrx/store'; | |
21 | +import { AppState } from '@core/core.state'; | |
22 | +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; | |
23 | +import { Router } from '@angular/router'; | |
24 | +import { DialogComponent } from '@app/shared/components/dialog.component'; | |
25 | +import { UtilsService } from '@core/services/utils.service'; | |
26 | +import { TranslateService } from '@ngx-translate/core'; | |
27 | + | |
28 | +export interface EditAlarmDetailsDialogData { | |
29 | + alarmDetails: string; | |
30 | +} | |
31 | + | |
32 | +@Component({ | |
33 | + selector: 'tb-edit-alarm-details-dialog', | |
34 | + templateUrl: './edit-alarm-details-dialog.component.html', | |
35 | + providers: [{provide: ErrorStateMatcher, useExisting: EditAlarmDetailsDialogComponent}], | |
36 | + styleUrls: [] | |
37 | +}) | |
38 | +export class EditAlarmDetailsDialogComponent extends DialogComponent<EditAlarmDetailsDialogComponent, string> | |
39 | + implements OnInit, ErrorStateMatcher { | |
40 | + | |
41 | + alarmDetails = this.data.alarmDetails; | |
42 | + | |
43 | + editDetailsFormGroup: FormGroup; | |
44 | + | |
45 | + submitted = false; | |
46 | + | |
47 | + constructor(protected store: Store<AppState>, | |
48 | + protected router: Router, | |
49 | + @Inject(MAT_DIALOG_DATA) public data: EditAlarmDetailsDialogData, | |
50 | + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | |
51 | + public dialogRef: MatDialogRef<EditAlarmDetailsDialogComponent, string>, | |
52 | + private fb: FormBuilder, | |
53 | + private utils: UtilsService, | |
54 | + public translate: TranslateService) { | |
55 | + super(store, router, dialogRef); | |
56 | + | |
57 | + this.editDetailsFormGroup = this.fb.group({ | |
58 | + alarmDetails: [this.alarmDetails] | |
59 | + }); | |
60 | + } | |
61 | + | |
62 | + ngOnInit(): void { | |
63 | + } | |
64 | + | |
65 | + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | |
66 | + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); | |
67 | + const customErrorState = !!(control && control.invalid && this.submitted); | |
68 | + return originalErrorState || customErrorState; | |
69 | + } | |
70 | + | |
71 | + cancel(): void { | |
72 | + this.dialogRef.close(null); | |
73 | + } | |
74 | + | |
75 | + save(): void { | |
76 | + this.submitted = true; | |
77 | + this.alarmDetails = this.editDetailsFormGroup.get('alarmDetails').value; | |
78 | + this.dialogRef.close(this.alarmDetails); | |
79 | + } | |
80 | +} | ... | ... |
... | ... | @@ -266,11 +266,11 @@ export class DeviceWizardDialogComponent extends |
266 | 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" | ... | ... |