Commit c9f3af73fd352666c5c47a66e12c3ee44c6f0fd8

Authored by Andrii Shvaika
1 parent dafc0beb

Scheduler for Device Profile Alarms

@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 */ 15 */
16 package org.thingsboard.server.common.data.device.profile; 16 package org.thingsboard.server.common.data.device.profile;
17 17
  18 +import com.fasterxml.jackson.annotation.JsonIgnore;
18 import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 19 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
19 import com.fasterxml.jackson.annotation.JsonSubTypes; 20 import com.fasterxml.jackson.annotation.JsonSubTypes;
20 import com.fasterxml.jackson.annotation.JsonTypeInfo; 21 import com.fasterxml.jackson.annotation.JsonTypeInfo;
@@ -30,6 +31,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -30,6 +31,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
30 @JsonSubTypes.Type(value = RepeatingAlarmConditionSpec.class, name = "REPEATING")}) 31 @JsonSubTypes.Type(value = RepeatingAlarmConditionSpec.class, name = "REPEATING")})
31 public interface AlarmConditionSpec { 32 public interface AlarmConditionSpec {
32 33
  34 + @JsonIgnore
33 AlarmConditionSpecType getType(); 35 AlarmConditionSpecType getType();
34 36
35 } 37 }
@@ -23,7 +23,7 @@ import java.util.List; @@ -23,7 +23,7 @@ import java.util.List;
23 public class CustomTimeScheduleItem { 23 public class CustomTimeScheduleItem {
24 24
25 private boolean enabled; 25 private boolean enabled;
26 - private Integer dayOfWeek; 26 + private int dayOfWeek;
27 private long startsOn; 27 private long startsOn;
28 private long endsOn; 28 private long endsOn;
29 29
@@ -29,6 +29,6 @@ public class DurationAlarmConditionSpec implements AlarmConditionSpec { @@ -29,6 +29,6 @@ public class DurationAlarmConditionSpec implements AlarmConditionSpec {
29 29
30 @Override 30 @Override
31 public AlarmConditionSpecType getType() { 31 public AlarmConditionSpecType getType() {
32 - return AlarmConditionSpecType.SIMPLE; 32 + return AlarmConditionSpecType.DURATION;
33 } 33 }
34 } 34 }
@@ -28,6 +28,6 @@ public class RepeatingAlarmConditionSpec implements AlarmConditionSpec { @@ -28,6 +28,6 @@ public class RepeatingAlarmConditionSpec implements AlarmConditionSpec {
28 28
29 @Override 29 @Override
30 public AlarmConditionSpecType getType() { 30 public AlarmConditionSpecType getType() {
31 - return AlarmConditionSpecType.SIMPLE; 31 + return AlarmConditionSpecType.REPEATING;
32 } 32 }
33 } 33 }
@@ -18,12 +18,13 @@ package org.thingsboard.server.common.data.device.profile; @@ -18,12 +18,13 @@ package org.thingsboard.server.common.data.device.profile;
18 import lombok.Data; 18 import lombok.Data;
19 19
20 import java.util.List; 20 import java.util.List;
  21 +import java.util.Set;
21 22
22 @Data 23 @Data
23 public class SpecificTimeSchedule implements AlarmSchedule { 24 public class SpecificTimeSchedule implements AlarmSchedule {
24 25
25 private String timezone; 26 private String timezone;
26 - private List<Integer> daysOfWeek; 27 + private Set<Integer> daysOfWeek;
27 private long startsOn; 28 private long startsOn;
28 private long endsOn; 29 private long endsOn;
29 30
  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.server.common.msg.tools;
  17 +
  18 +import java.time.ZoneId;
  19 +import java.util.concurrent.ConcurrentHashMap;
  20 +import java.util.concurrent.ConcurrentMap;
  21 +
  22 +public class SchedulerUtils {
  23 +
  24 + private static final ConcurrentMap<String, ZoneId> tzMap = new ConcurrentHashMap<>();
  25 +
  26 + public static ZoneId getZoneId(String tz) {
  27 + return tzMap.computeIfAbsent(tz == null || tz.isEmpty() ? "UTC" : tz, ZoneId::of);
  28 + }
  29 +
  30 +}
@@ -21,15 +21,24 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity; @@ -21,15 +21,24 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity;
21 import org.thingsboard.server.common.data.device.profile.AlarmCondition; 21 import org.thingsboard.server.common.data.device.profile.AlarmCondition;
22 import org.thingsboard.server.common.data.device.profile.AlarmConditionSpec; 22 import org.thingsboard.server.common.data.device.profile.AlarmConditionSpec;
23 import org.thingsboard.server.common.data.device.profile.AlarmRule; 23 import org.thingsboard.server.common.data.device.profile.AlarmRule;
  24 +import org.thingsboard.server.common.data.device.profile.CustomTimeSchedule;
  25 +import org.thingsboard.server.common.data.device.profile.CustomTimeScheduleItem;
24 import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec; 26 import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec;
25 import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec; 27 import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec;
26 import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; 28 import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec;
  29 +import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule;
27 import org.thingsboard.server.common.data.query.BooleanFilterPredicate; 30 import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
28 import org.thingsboard.server.common.data.query.ComplexFilterPredicate; 31 import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
29 import org.thingsboard.server.common.data.query.KeyFilter; 32 import org.thingsboard.server.common.data.query.KeyFilter;
30 import org.thingsboard.server.common.data.query.KeyFilterPredicate; 33 import org.thingsboard.server.common.data.query.KeyFilterPredicate;
31 import org.thingsboard.server.common.data.query.NumericFilterPredicate; 34 import org.thingsboard.server.common.data.query.NumericFilterPredicate;
32 import org.thingsboard.server.common.data.query.StringFilterPredicate; 35 import org.thingsboard.server.common.data.query.StringFilterPredicate;
  36 +import org.thingsboard.server.common.msg.tools.SchedulerUtils;
  37 +
  38 +import java.time.Instant;
  39 +import java.time.ZoneId;
  40 +import java.time.ZonedDateTime;
  41 +import java.util.Calendar;
33 42
34 @Data 43 @Data
35 public class AlarmRuleState { 44 public class AlarmRuleState {
@@ -85,21 +94,72 @@ public class AlarmRuleState { @@ -85,21 +94,72 @@ public class AlarmRuleState {
85 } 94 }
86 95
87 public boolean eval(DeviceDataSnapshot data) { 96 public boolean eval(DeviceDataSnapshot data) {
  97 + boolean active = isActive(data.getTs());
88 switch (spec.getType()) { 98 switch (spec.getType()) {
89 case SIMPLE: 99 case SIMPLE:
90 - return eval(alarmRule.getCondition(), data); 100 + return active && eval(alarmRule.getCondition(), data);
91 case DURATION: 101 case DURATION:
92 - return evalDuration(data); 102 + return evalDuration(data, active);
93 case REPEATING: 103 case REPEATING:
94 - return evalRepeating(data); 104 + return evalRepeating(data, active);
95 default: 105 default:
96 return false; 106 return false;
97 } 107 }
98 } 108 }
99 109
100 - private boolean evalRepeating(DeviceDataSnapshot data) {  
101 - boolean eval = eval(alarmRule.getCondition(), data);  
102 - if (eval) { 110 + private boolean isActive(long eventTs) {
  111 + if (eventTs == 0L) {
  112 + eventTs = System.currentTimeMillis();
  113 + }
  114 + if (alarmRule.getSchedule() == null) {
  115 + return true;
  116 + }
  117 + switch (alarmRule.getSchedule().getType()) {
  118 + case ANY_TIME:
  119 + return true;
  120 + case SPECIFIC_TIME:
  121 + return isActiveSpecific((SpecificTimeSchedule) alarmRule.getSchedule(), eventTs);
  122 + case CUSTOM:
  123 + return isActiveCustom((CustomTimeSchedule) alarmRule.getSchedule(), eventTs);
  124 + default:
  125 + throw new RuntimeException("Unsupported schedule type: " + alarmRule.getSchedule().getType());
  126 + }
  127 + }
  128 +
  129 + private boolean isActiveSpecific(SpecificTimeSchedule schedule, long eventTs) {
  130 + ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone());
  131 + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId);
  132 + if (schedule.getDaysOfWeek().size() != 7) {
  133 + int dayOfWeek = zdt.getDayOfWeek().getValue();
  134 + if (!schedule.getDaysOfWeek().contains(dayOfWeek)) {
  135 + return false;
  136 + }
  137 + }
  138 + long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli();
  139 + long msFromStartOfDay = eventTs - startOfDay;
  140 + return schedule.getStartsOn() <= msFromStartOfDay && schedule.getEndsOn() > msFromStartOfDay;
  141 + }
  142 +
  143 + private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) {
  144 + ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone());
  145 + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId);
  146 + int dayOfWeek = zdt.toLocalDate().getDayOfWeek().getValue();
  147 + for (CustomTimeScheduleItem item : schedule.getItems()) {
  148 + if (item.getDayOfWeek() == dayOfWeek) {
  149 + 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;
  153 + } else {
  154 + return false;
  155 + }
  156 + }
  157 + }
  158 + return false;
  159 + }
  160 +
  161 + private boolean evalRepeating(DeviceDataSnapshot data, boolean active) {
  162 + if (active && eval(alarmRule.getCondition(), data)) {
103 state.setEventCount(state.getEventCount() + 1); 163 state.setEventCount(state.getEventCount() + 1);
104 updateFlag = true; 164 updateFlag = true;
105 return state.getEventCount() > requiredRepeats; 165 return state.getEventCount() > requiredRepeats;
@@ -112,9 +172,8 @@ public class AlarmRuleState { @@ -112,9 +172,8 @@ public class AlarmRuleState {
112 } 172 }
113 } 173 }
114 174
115 - private boolean evalDuration(DeviceDataSnapshot data) {  
116 - boolean eval = eval(alarmRule.getCondition(), data);  
117 - if (eval) { 175 + private boolean evalDuration(DeviceDataSnapshot data, boolean active) {
  176 + if (active && eval(alarmRule.getCondition(), data)) {
118 if (state.getLastEventTs() > 0) { 177 if (state.getLastEventTs() > 0) {
119 if (data.getTs() > state.getLastEventTs()) { 178 if (data.getTs() > state.getLastEventTs()) {
120 state.setDuration(state.getDuration() + (data.getTs() - state.getLastEventTs())); 179 state.setDuration(state.getDuration() + (data.getTs() - state.getLastEventTs()));
@@ -145,7 +204,13 @@ public class AlarmRuleState { @@ -145,7 +204,13 @@ public class AlarmRuleState {
145 case DURATION: 204 case DURATION:
146 if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && ts > state.getLastEventTs()) { 205 if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && ts > state.getLastEventTs()) {
147 long duration = state.getDuration() + (ts - state.getLastEventTs()); 206 long duration = state.getDuration() + (ts - state.getLastEventTs());
148 - return duration > requiredDurationInMs; 207 + boolean result = duration > requiredDurationInMs && isActive(ts);
  208 + if (result) {
  209 + state.setLastEventTs(0L);
  210 + state.setDuration(0L);
  211 + updateFlag = true;
  212 + }
  213 + return result;
149 } 214 }
150 default: 215 default:
151 return false; 216 return false;