From c9f3af73fd352666c5c47a66e12c3ee44c6f0fd8 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Mon, 5 Oct 2020 18:53:15 +0300 Subject: [PATCH] Scheduler for Device Profile Alarms --- .../device/profile/AlarmConditionSpec.java | 2 + .../profile/CustomTimeScheduleItem.java | 2 +- .../profile/DurationAlarmConditionSpec.java | 2 +- .../profile/RepeatingAlarmConditionSpec.java | 2 +- .../device/profile/SpecificTimeSchedule.java | 3 +- .../common/msg/tools/SchedulerUtils.java | 30 +++++++ .../rule/engine/profile/AlarmRuleState.java | 85 ++++++++++++++++--- 7 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java index 8c3e841707..915f62681b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.device.profile; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -30,6 +31,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = RepeatingAlarmConditionSpec.class, name = "REPEATING")}) public interface AlarmConditionSpec { + @JsonIgnore AlarmConditionSpecType getType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java index b38ec32fc9..ba5735988d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java @@ -23,7 +23,7 @@ import java.util.List; public class CustomTimeScheduleItem { private boolean enabled; - private Integer dayOfWeek; + private int dayOfWeek; private long startsOn; private long endsOn; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java index 459274aa59..cb27e45538 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java @@ -29,6 +29,6 @@ public class DurationAlarmConditionSpec implements AlarmConditionSpec { @Override public AlarmConditionSpecType getType() { - return AlarmConditionSpecType.SIMPLE; + return AlarmConditionSpecType.DURATION; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java index e79a6ff265..3883676ee5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java @@ -28,6 +28,6 @@ public class RepeatingAlarmConditionSpec implements AlarmConditionSpec { @Override public AlarmConditionSpecType getType() { - return AlarmConditionSpecType.SIMPLE; + return AlarmConditionSpecType.REPEATING; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java index 35d5c03057..c099b57680 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java @@ -18,12 +18,13 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; import java.util.List; +import java.util.Set; @Data public class SpecificTimeSchedule implements AlarmSchedule { private String timezone; - private List daysOfWeek; + private Set daysOfWeek; private long startsOn; private long endsOn; diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java new file mode 100644 index 0000000000..fea6fcb337 --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.msg.tools; + +import java.time.ZoneId; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class SchedulerUtils { + + private static final ConcurrentMap tzMap = new ConcurrentHashMap<>(); + + public static ZoneId getZoneId(String tz) { + return tzMap.computeIfAbsent(tz == null || tz.isEmpty() ? "UTC" : tz, ZoneId::of); + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java index bd921ac931..255791e01b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java @@ -21,15 +21,24 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.device.profile.AlarmCondition; import org.thingsboard.server.common.data.device.profile.AlarmConditionSpec; import org.thingsboard.server.common.data.device.profile.AlarmRule; +import org.thingsboard.server.common.data.device.profile.CustomTimeSchedule; +import org.thingsboard.server.common.data.device.profile.CustomTimeScheduleItem; import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec; import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec; import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; +import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.KeyFilterPredicate; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.common.msg.tools.SchedulerUtils; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Calendar; @Data public class AlarmRuleState { @@ -85,21 +94,72 @@ public class AlarmRuleState { } public boolean eval(DeviceDataSnapshot data) { + boolean active = isActive(data.getTs()); switch (spec.getType()) { case SIMPLE: - return eval(alarmRule.getCondition(), data); + return active && eval(alarmRule.getCondition(), data); case DURATION: - return evalDuration(data); + return evalDuration(data, active); case REPEATING: - return evalRepeating(data); + return evalRepeating(data, active); default: return false; } } - private boolean evalRepeating(DeviceDataSnapshot data) { - boolean eval = eval(alarmRule.getCondition(), data); - if (eval) { + private boolean isActive(long eventTs) { + if (eventTs == 0L) { + eventTs = System.currentTimeMillis(); + } + if (alarmRule.getSchedule() == null) { + return true; + } + switch (alarmRule.getSchedule().getType()) { + case ANY_TIME: + return true; + case SPECIFIC_TIME: + return isActiveSpecific((SpecificTimeSchedule) alarmRule.getSchedule(), eventTs); + case CUSTOM: + return isActiveCustom((CustomTimeSchedule) alarmRule.getSchedule(), eventTs); + default: + throw new RuntimeException("Unsupported schedule type: " + alarmRule.getSchedule().getType()); + } + } + + private boolean isActiveSpecific(SpecificTimeSchedule schedule, long eventTs) { + ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone()); + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId); + if (schedule.getDaysOfWeek().size() != 7) { + int dayOfWeek = zdt.getDayOfWeek().getValue(); + if (!schedule.getDaysOfWeek().contains(dayOfWeek)) { + return false; + } + } + long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli(); + long msFromStartOfDay = eventTs - startOfDay; + return schedule.getStartsOn() <= msFromStartOfDay && schedule.getEndsOn() > msFromStartOfDay; + } + + private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) { + ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone()); + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId); + int dayOfWeek = zdt.toLocalDate().getDayOfWeek().getValue(); + for (CustomTimeScheduleItem item : schedule.getItems()) { + if (item.getDayOfWeek() == dayOfWeek) { + if (item.isEnabled()) { + long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli(); + long msFromStartOfDay = eventTs - startOfDay; + return item.getStartsOn() <= msFromStartOfDay && item.getEndsOn() > msFromStartOfDay; + } else { + return false; + } + } + } + return false; + } + + private boolean evalRepeating(DeviceDataSnapshot data, boolean active) { + if (active && eval(alarmRule.getCondition(), data)) { state.setEventCount(state.getEventCount() + 1); updateFlag = true; return state.getEventCount() > requiredRepeats; @@ -112,9 +172,8 @@ public class AlarmRuleState { } } - private boolean evalDuration(DeviceDataSnapshot data) { - boolean eval = eval(alarmRule.getCondition(), data); - if (eval) { + private boolean evalDuration(DeviceDataSnapshot data, boolean active) { + if (active && eval(alarmRule.getCondition(), data)) { if (state.getLastEventTs() > 0) { if (data.getTs() > state.getLastEventTs()) { state.setDuration(state.getDuration() + (data.getTs() - state.getLastEventTs())); @@ -145,7 +204,13 @@ public class AlarmRuleState { case DURATION: if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && ts > state.getLastEventTs()) { long duration = state.getDuration() + (ts - state.getLastEventTs()); - return duration > requiredDurationInMs; + boolean result = duration > requiredDurationInMs && isActive(ts); + if (result) { + state.setLastEventTs(0L); + state.setDuration(0L); + updateFlag = true; + } + return result; } default: return false;