Merge pull request #5818 from desoliture1/alarmScheduleDynamicValues
[3.4] Add support for dynamic values for schedules in alarm rules
This commit is contained in:
commit
44749f1306
@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.device.profile;
|
|||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
|
import org.thingsboard.server.common.data.query.DynamicValue;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
@ -34,4 +35,6 @@ public interface AlarmSchedule extends Serializable {
|
|||||||
|
|
||||||
AlarmScheduleType getType();
|
AlarmScheduleType getType();
|
||||||
|
|
||||||
|
DynamicValue<String> getDynamicValue();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.common.data.device.profile;
|
package org.thingsboard.server.common.data.device.profile;
|
||||||
|
|
||||||
|
import org.thingsboard.server.common.data.query.DynamicValue;
|
||||||
|
|
||||||
public class AnyTimeSchedule implements AlarmSchedule {
|
public class AnyTimeSchedule implements AlarmSchedule {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -22,4 +24,9 @@ public class AnyTimeSchedule implements AlarmSchedule {
|
|||||||
return AlarmScheduleType.ANY_TIME;
|
return AlarmScheduleType.ANY_TIME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DynamicValue<String> getDynamicValue() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
package org.thingsboard.server.common.data.device.profile;
|
package org.thingsboard.server.common.data.device.profile;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.thingsboard.server.common.data.query.DynamicValue;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -25,6 +26,8 @@ public class CustomTimeSchedule implements AlarmSchedule {
|
|||||||
private String timezone;
|
private String timezone;
|
||||||
private List<CustomTimeScheduleItem> items;
|
private List<CustomTimeScheduleItem> items;
|
||||||
|
|
||||||
|
private DynamicValue<String> dynamicValue;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AlarmScheduleType getType() {
|
public AlarmScheduleType getType() {
|
||||||
return AlarmScheduleType.CUSTOM;
|
return AlarmScheduleType.CUSTOM;
|
||||||
|
|||||||
@ -16,8 +16,8 @@
|
|||||||
package org.thingsboard.server.common.data.device.profile;
|
package org.thingsboard.server.common.data.device.profile;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.thingsboard.server.common.data.query.DynamicValue;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@ -28,6 +28,8 @@ public class SpecificTimeSchedule implements AlarmSchedule {
|
|||||||
private long startsOn;
|
private long startsOn;
|
||||||
private long endsOn;
|
private long endsOn;
|
||||||
|
|
||||||
|
private DynamicValue<String> dynamicValue;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AlarmScheduleType getType() {
|
public AlarmScheduleType getType() {
|
||||||
return AlarmScheduleType.SPECIFIC_TIME;
|
return AlarmScheduleType.SPECIFIC_TIME;
|
||||||
|
|||||||
@ -575,6 +575,10 @@ public class JsonConverter {
|
|||||||
return JSON_PARSER.parse(json);
|
return JSON_PARSER.parse(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> T parse(String json, Class<T> clazz) {
|
||||||
|
return fromJson(parse(json), clazz);
|
||||||
|
}
|
||||||
|
|
||||||
public static String toJson(JsonElement element) {
|
public static String toJson(JsonElement element) {
|
||||||
return GSON.toJson(element);
|
return GSON.toJson(element);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionSpec;
|
|||||||
import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType;
|
import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType;
|
||||||
import org.thingsboard.server.common.data.device.profile.AlarmRule;
|
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.CustomTimeSchedule;
|
||||||
|
import org.thingsboard.server.common.data.device.profile.AlarmSchedule;
|
||||||
import org.thingsboard.server.common.data.device.profile.CustomTimeScheduleItem;
|
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.DurationAlarmConditionSpec;
|
||||||
import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec;
|
import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec;
|
||||||
@ -40,6 +41,7 @@ import org.thingsboard.server.common.data.query.KeyFilterPredicate;
|
|||||||
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
|
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
|
||||||
import org.thingsboard.server.common.data.query.StringFilterPredicate;
|
import org.thingsboard.server.common.data.query.StringFilterPredicate;
|
||||||
import org.thingsboard.server.common.msg.tools.SchedulerUtils;
|
import org.thingsboard.server.common.msg.tools.SchedulerUtils;
|
||||||
|
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
@ -115,7 +117,7 @@ class AlarmRuleState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public AlarmEvalResult eval(DataSnapshot data) {
|
public AlarmEvalResult eval(DataSnapshot data) {
|
||||||
boolean active = isActive(data.getTs());
|
boolean active = isActive(data, data.getTs());
|
||||||
switch (spec.getType()) {
|
switch (spec.getType()) {
|
||||||
case SIMPLE:
|
case SIMPLE:
|
||||||
return (active && eval(alarmRule.getCondition(), data)) ? AlarmEvalResult.TRUE : AlarmEvalResult.FALSE;
|
return (active && eval(alarmRule.getCondition(), data)) ? AlarmEvalResult.TRUE : AlarmEvalResult.FALSE;
|
||||||
@ -128,7 +130,7 @@ class AlarmRuleState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isActive(long eventTs) {
|
private boolean isActive(DataSnapshot data, long eventTs) {
|
||||||
if (eventTs == 0L) {
|
if (eventTs == 0L) {
|
||||||
eventTs = System.currentTimeMillis();
|
eventTs = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
@ -139,14 +141,28 @@ class AlarmRuleState {
|
|||||||
case ANY_TIME:
|
case ANY_TIME:
|
||||||
return true;
|
return true;
|
||||||
case SPECIFIC_TIME:
|
case SPECIFIC_TIME:
|
||||||
return isActiveSpecific((SpecificTimeSchedule) alarmRule.getSchedule(), eventTs);
|
return isActiveSpecific((SpecificTimeSchedule) getSchedule(data, alarmRule), eventTs);
|
||||||
case CUSTOM:
|
case CUSTOM:
|
||||||
return isActiveCustom((CustomTimeSchedule) alarmRule.getSchedule(), eventTs);
|
return isActiveCustom((CustomTimeSchedule) getSchedule(data, alarmRule), eventTs);
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Unsupported schedule type: " + alarmRule.getSchedule().getType());
|
throw new RuntimeException("Unsupported schedule type: " + alarmRule.getSchedule().getType());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AlarmSchedule getSchedule(DataSnapshot data, AlarmRule alarmRule) {
|
||||||
|
AlarmSchedule schedule = alarmRule.getSchedule();
|
||||||
|
EntityKeyValue dynamicValue = getDynamicPredicateValue(data, schedule.getDynamicValue());
|
||||||
|
|
||||||
|
if (dynamicValue != null) {
|
||||||
|
try {
|
||||||
|
return JsonConverter.parse(dynamicValue.getJsonValue(), alarmRule.getSchedule().getClass());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.trace("Failed to parse AlarmSchedule from dynamicValue: {}", dynamicValue.getJsonValue(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return schedule;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isActiveSpecific(SpecificTimeSchedule schedule, long eventTs) {
|
private boolean isActiveSpecific(SpecificTimeSchedule schedule, long eventTs) {
|
||||||
ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone());
|
ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone());
|
||||||
ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId);
|
ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId);
|
||||||
@ -156,7 +172,13 @@ class AlarmRuleState {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return isActive(eventTs, zoneId, zdt, schedule.getStartsOn(), schedule.getEndsOn());
|
long endsOn = schedule.getEndsOn();
|
||||||
|
if (endsOn == 0) {
|
||||||
|
// 24 hours in milliseconds
|
||||||
|
endsOn = 86400000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isActive(eventTs, zoneId, zdt, schedule.getStartsOn(), endsOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) {
|
private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) {
|
||||||
@ -166,7 +188,12 @@ class AlarmRuleState {
|
|||||||
for (CustomTimeScheduleItem item : schedule.getItems()) {
|
for (CustomTimeScheduleItem item : schedule.getItems()) {
|
||||||
if (item.getDayOfWeek() == dayOfWeek) {
|
if (item.getDayOfWeek() == dayOfWeek) {
|
||||||
if (item.isEnabled()) {
|
if (item.isEnabled()) {
|
||||||
return isActive(eventTs, zoneId, zdt, item.getStartsOn(), item.getEndsOn());
|
long endsOn = item.getEndsOn();
|
||||||
|
if (endsOn == 0) {
|
||||||
|
// 24 hours in milliseconds
|
||||||
|
endsOn = 86400000;
|
||||||
|
}
|
||||||
|
return isActive(eventTs, zoneId, zdt, item.getStartsOn(), endsOn);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -279,7 +306,7 @@ class AlarmRuleState {
|
|||||||
long requiredDurationInMs = resolveRequiredDurationInMs(dataSnapshot);
|
long requiredDurationInMs = resolveRequiredDurationInMs(dataSnapshot);
|
||||||
if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && ts > state.getLastEventTs()) {
|
if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && ts > state.getLastEventTs()) {
|
||||||
long duration = state.getDuration() + (ts - state.getLastEventTs());
|
long duration = state.getDuration() + (ts - state.getLastEventTs());
|
||||||
if (isActive(ts)) {
|
if (isActive(dataSnapshot, ts)) {
|
||||||
return duration > requiredDurationInMs ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE;
|
return duration > requiredDurationInMs ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE;
|
||||||
} else {
|
} else {
|
||||||
return AlarmEvalResult.FALSE;
|
return AlarmEvalResult.FALSE;
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.device.profile.AlarmRule;
|
|||||||
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
|
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
|
||||||
import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec;
|
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.RepeatingAlarmConditionSpec;
|
||||||
|
import org.thingsboard.server.common.data.device.profile.AlarmSchedule;
|
||||||
import org.thingsboard.server.common.data.id.DeviceProfileId;
|
import org.thingsboard.server.common.data.id.DeviceProfileId;
|
||||||
import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
|
import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
|
||||||
import org.thingsboard.server.common.data.query.DynamicValue;
|
import org.thingsboard.server.common.data.query.DynamicValue;
|
||||||
@ -77,6 +78,10 @@ class ProfileState {
|
|||||||
addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, ruleKeys);
|
addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, ruleKeys);
|
||||||
}
|
}
|
||||||
addEntityKeysFromAlarmConditionSpec(alarmRule);
|
addEntityKeysFromAlarmConditionSpec(alarmRule);
|
||||||
|
AlarmSchedule schedule = alarmRule.getSchedule();
|
||||||
|
if (schedule != null) {
|
||||||
|
addScheduleDynamicValues(schedule);
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
if (alarm.getClearRule() != null) {
|
if (alarm.getClearRule() != null) {
|
||||||
var clearAlarmKeys = alarmClearKeys.computeIfAbsent(alarm.getId(), id -> new HashSet<>());
|
var clearAlarmKeys = alarmClearKeys.computeIfAbsent(alarm.getId(), id -> new HashSet<>());
|
||||||
@ -91,6 +96,16 @@ class ProfileState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addScheduleDynamicValues(AlarmSchedule schedule) {
|
||||||
|
DynamicValue<String> dynamicValue = schedule.getDynamicValue();
|
||||||
|
if (dynamicValue != null) {
|
||||||
|
entityKeys.add(
|
||||||
|
new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE,
|
||||||
|
dynamicValue.getSourceAttribute())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void addEntityKeysFromAlarmConditionSpec(AlarmRule alarmRule) {
|
private void addEntityKeysFromAlarmConditionSpec(AlarmRule alarmRule) {
|
||||||
AlarmConditionSpec spec = alarmRule.getCondition().getSpec();
|
AlarmConditionSpec spec = alarmRule.getCondition().getSpec();
|
||||||
if (spec == null) {
|
if (spec == null) {
|
||||||
|
|||||||
@ -44,6 +44,8 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
|
|||||||
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
|
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
|
||||||
import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec;
|
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.RepeatingAlarmConditionSpec;
|
||||||
|
import org.thingsboard.server.common.data.device.profile.CustomTimeSchedule;
|
||||||
|
import org.thingsboard.server.common.data.device.profile.CustomTimeScheduleItem;
|
||||||
import org.thingsboard.server.common.data.id.CustomerId;
|
import org.thingsboard.server.common.data.id.CustomerId;
|
||||||
import org.thingsboard.server.common.data.id.DeviceId;
|
import org.thingsboard.server.common.data.id.DeviceId;
|
||||||
import org.thingsboard.server.common.data.id.DeviceProfileId;
|
import org.thingsboard.server.common.data.id.DeviceProfileId;
|
||||||
@ -71,6 +73,7 @@ import java.math.RoundingMode;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@ -1086,6 +1089,179 @@ public class TbDeviceProfileNodeTest {
|
|||||||
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
|
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testActiveAlarmScheduleFromDynamicValuesWhenDefaultScheduleIsInactive() throws Exception {
|
||||||
|
init();
|
||||||
|
|
||||||
|
DeviceProfile deviceProfile = new DeviceProfile();
|
||||||
|
deviceProfile.setId(deviceProfileId);
|
||||||
|
DeviceProfileData deviceProfileData = new DeviceProfileData();
|
||||||
|
|
||||||
|
Device device = new Device();
|
||||||
|
device.setId(deviceId);
|
||||||
|
device.setCustomerId(customerId);
|
||||||
|
|
||||||
|
AttributeKvCompositeKey compositeKeyActiveSchedule = new AttributeKvCompositeKey(
|
||||||
|
EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "dynamicValueActiveSchedule"
|
||||||
|
);
|
||||||
|
|
||||||
|
AttributeKvEntity attributeKvEntityActiveSchedule = new AttributeKvEntity();
|
||||||
|
attributeKvEntityActiveSchedule.setId(compositeKeyActiveSchedule);
|
||||||
|
attributeKvEntityActiveSchedule.setJsonValue(
|
||||||
|
"{\"timezone\":\"Europe/Kiev\",\"items\":[{\"enabled\":true,\"dayOfWeek\":1,\"startsOn\":0,\"endsOn\":8.64e+7},{\"enabled\":true,\"dayOfWeek\":2,\"startsOn\":0,\"endsOn\":8.64e+7},{\"enabled\":true,\"dayOfWeek\":3,\"startsOn\":0,\"endsOn\":8.64e+7},{\"enabled\":true,\"dayOfWeek\":4,\"startsOn\":0,\"endsOn\":8.64e+7},{\"enabled\":true,\"dayOfWeek\":5,\"startsOn\":0,\"endsOn\":8.64e+7},{\"enabled\":true,\"dayOfWeek\":6,\"startsOn\":8.64e+7,\"endsOn\":8.64e+7},{\"enabled\":true,\"dayOfWeek\":7,\"startsOn\":0,\"endsOn\":8.64e+7}],\"dynamicValue\":null}"
|
||||||
|
);
|
||||||
|
attributeKvEntityActiveSchedule.setLastUpdateTs(0L);
|
||||||
|
|
||||||
|
AttributeKvEntry entryActiveSchedule = attributeKvEntityActiveSchedule.toData();
|
||||||
|
|
||||||
|
ListenableFuture<List<AttributeKvEntry>> listListenableFutureActiveSchedule =
|
||||||
|
Futures.immediateFuture(Collections.singletonList(entryActiveSchedule));
|
||||||
|
|
||||||
|
AlarmConditionFilter highTempFilter = new AlarmConditionFilter();
|
||||||
|
highTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature"));
|
||||||
|
highTempFilter.setValueType(EntityKeyValueType.NUMERIC);
|
||||||
|
NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate();
|
||||||
|
highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
|
||||||
|
highTemperaturePredicate.setValue(new FilterPredicateValue<>(
|
||||||
|
0.0,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
));
|
||||||
|
highTempFilter.setPredicate(highTemperaturePredicate);
|
||||||
|
AlarmCondition alarmCondition = new AlarmCondition();
|
||||||
|
alarmCondition.setCondition(Collections.singletonList(highTempFilter));
|
||||||
|
|
||||||
|
CustomTimeSchedule schedule = new CustomTimeSchedule();
|
||||||
|
schedule.setItems(Collections.emptyList());
|
||||||
|
schedule.setDynamicValue(new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "dynamicValueActiveSchedule", false));
|
||||||
|
|
||||||
|
AlarmRule alarmRule = new AlarmRule();
|
||||||
|
alarmRule.setCondition(alarmCondition);
|
||||||
|
alarmRule.setSchedule(schedule);
|
||||||
|
DeviceProfileAlarm deviceProfileAlarmActiveSchedule = new DeviceProfileAlarm();
|
||||||
|
deviceProfileAlarmActiveSchedule.setId("highTemperatureAlarmID");
|
||||||
|
deviceProfileAlarmActiveSchedule.setAlarmType("highTemperatureAlarm");
|
||||||
|
deviceProfileAlarmActiveSchedule.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule)));
|
||||||
|
|
||||||
|
deviceProfileData.setAlarms(Collections.singletonList(deviceProfileAlarmActiveSchedule));
|
||||||
|
deviceProfile.setProfileData(deviceProfileData);
|
||||||
|
|
||||||
|
Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile);
|
||||||
|
Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature")))
|
||||||
|
.thenReturn(Futures.immediateFuture(Collections.emptyList()));
|
||||||
|
Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm"))
|
||||||
|
.thenReturn(Futures.immediateFuture(null));
|
||||||
|
Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg());
|
||||||
|
Mockito.when(ctx.getAttributesService()).thenReturn(attributesService);
|
||||||
|
Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet()))
|
||||||
|
.thenReturn(listListenableFutureActiveSchedule);
|
||||||
|
|
||||||
|
TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "");
|
||||||
|
Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString()))
|
||||||
|
.thenReturn(theMsg);
|
||||||
|
|
||||||
|
ObjectNode data = mapper.createObjectNode();
|
||||||
|
data.put("temperature", 35);
|
||||||
|
TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(),
|
||||||
|
TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
|
||||||
|
|
||||||
|
node.onMsg(ctx, msg);
|
||||||
|
verify(ctx).tellSuccess(msg);
|
||||||
|
verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
|
||||||
|
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInactiveAlarmScheduleFromDynamicValuesWhenDefaultScheduleIsActive() throws Exception {
|
||||||
|
init();
|
||||||
|
|
||||||
|
DeviceProfile deviceProfile = new DeviceProfile();
|
||||||
|
deviceProfile.setId(deviceProfileId);
|
||||||
|
DeviceProfileData deviceProfileData = new DeviceProfileData();
|
||||||
|
|
||||||
|
Device device = new Device();
|
||||||
|
device.setId(deviceId);
|
||||||
|
device.setCustomerId(customerId);
|
||||||
|
|
||||||
|
AttributeKvCompositeKey compositeKeyInactiveSchedule = new AttributeKvCompositeKey(
|
||||||
|
EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "dynamicValueInactiveSchedule"
|
||||||
|
);
|
||||||
|
|
||||||
|
AttributeKvEntity attributeKvEntityInactiveSchedule = new AttributeKvEntity();
|
||||||
|
attributeKvEntityInactiveSchedule.setId(compositeKeyInactiveSchedule);
|
||||||
|
attributeKvEntityInactiveSchedule.setJsonValue(
|
||||||
|
"{\"timezone\":\"Europe/Kiev\",\"items\":[{\"enabled\":false,\"dayOfWeek\":1,\"startsOn\":0,\"endsOn\":0},{\"enabled\":false,\"dayOfWeek\":2,\"startsOn\":0,\"endsOn\":0},{\"enabled\":false,\"dayOfWeek\":3,\"startsOn\":0,\"endsOn\":0},{\"enabled\":false,\"dayOfWeek\":4,\"startsOn\":0,\"endsOn\":0},{\"enabled\":false,\"dayOfWeek\":5,\"startsOn\":0,\"endsOn\":0},{\"enabled\":false,\"dayOfWeek\":6,\"startsOn\":0,\"endsOn\":0},{\"enabled\":false,\"dayOfWeek\":7,\"startsOn\":0,\"endsOn\":0}],\"dynamicValue\":null}"
|
||||||
|
);
|
||||||
|
|
||||||
|
attributeKvEntityInactiveSchedule.setLastUpdateTs(0L);
|
||||||
|
|
||||||
|
AttributeKvEntry entryInactiveSchedule = attributeKvEntityInactiveSchedule.toData();
|
||||||
|
|
||||||
|
ListenableFuture<List<AttributeKvEntry>> listListenableFutureInactiveSchedule =
|
||||||
|
Futures.immediateFuture(Collections.singletonList(entryInactiveSchedule));
|
||||||
|
|
||||||
|
AlarmConditionFilter highTempFilter = new AlarmConditionFilter();
|
||||||
|
highTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature"));
|
||||||
|
highTempFilter.setValueType(EntityKeyValueType.NUMERIC);
|
||||||
|
NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate();
|
||||||
|
highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
|
||||||
|
highTemperaturePredicate.setValue(new FilterPredicateValue<>(
|
||||||
|
0.0,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
));
|
||||||
|
|
||||||
|
highTempFilter.setPredicate(highTemperaturePredicate);
|
||||||
|
AlarmCondition alarmCondition = new AlarmCondition();
|
||||||
|
alarmCondition.setCondition(Collections.singletonList(highTempFilter));
|
||||||
|
|
||||||
|
CustomTimeSchedule schedule = new CustomTimeSchedule();
|
||||||
|
|
||||||
|
List<CustomTimeScheduleItem> items = new ArrayList<>();
|
||||||
|
for (int i = 0; i < 7; i++) {
|
||||||
|
CustomTimeScheduleItem item = new CustomTimeScheduleItem();
|
||||||
|
item.setEnabled(true);
|
||||||
|
item.setDayOfWeek(i + 1);
|
||||||
|
item.setEndsOn(0);
|
||||||
|
item.setStartsOn(0);
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule.setItems(items);
|
||||||
|
schedule.setDynamicValue(new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "dynamicValueInactiveSchedule", false));
|
||||||
|
|
||||||
|
AlarmRule alarmRule = new AlarmRule();
|
||||||
|
alarmRule.setCondition(alarmCondition);
|
||||||
|
alarmRule.setSchedule(schedule);
|
||||||
|
DeviceProfileAlarm deviceProfileAlarmNonactiveSchedule = new DeviceProfileAlarm();
|
||||||
|
deviceProfileAlarmNonactiveSchedule.setId("highTemperatureAlarmID");
|
||||||
|
deviceProfileAlarmNonactiveSchedule.setAlarmType("highTemperatureAlarm");
|
||||||
|
deviceProfileAlarmNonactiveSchedule.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule)));
|
||||||
|
|
||||||
|
deviceProfileData.setAlarms(Collections.singletonList(deviceProfileAlarmNonactiveSchedule));
|
||||||
|
deviceProfile.setProfileData(deviceProfileData);
|
||||||
|
|
||||||
|
Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile);
|
||||||
|
Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature")))
|
||||||
|
.thenReturn(Futures.immediateFuture(Collections.emptyList()));
|
||||||
|
Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm"))
|
||||||
|
.thenReturn(Futures.immediateFuture(null));
|
||||||
|
Mockito.when(ctx.getAttributesService()).thenReturn(attributesService);
|
||||||
|
Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet()))
|
||||||
|
.thenReturn(listListenableFutureInactiveSchedule);
|
||||||
|
|
||||||
|
TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "");
|
||||||
|
|
||||||
|
ObjectNode data = mapper.createObjectNode();
|
||||||
|
data.put("temperature", 35);
|
||||||
|
TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(),
|
||||||
|
TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
|
||||||
|
|
||||||
|
node.onMsg(ctx, msg);
|
||||||
|
verify(ctx).tellSuccess(msg);
|
||||||
|
verify(ctx, Mockito.never()).enqueueForTellNext(theMsg, "Alarm Created");
|
||||||
|
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCurrentCustomersAttributeForDynamicValue() throws Exception {
|
public void testCurrentCustomersAttributeForDynamicValue() throws Exception {
|
||||||
|
|||||||
@ -148,6 +148,7 @@ import {
|
|||||||
HOME_COMPONENTS_MODULE_TOKEN
|
HOME_COMPONENTS_MODULE_TOKEN
|
||||||
} from '@home/components/tokens';
|
} from '@home/components/tokens';
|
||||||
import { DashboardStateComponent } from '@home/components/dashboard-page/dashboard-state.component';
|
import { DashboardStateComponent } from '@home/components/dashboard-page/dashboard-state.component';
|
||||||
|
import { AlarmDynamicValue } from '@home/components/profile/alarm/alarm-dynamic-value.component';
|
||||||
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
|
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
|
||||||
import { TenantProfileQueuesComponent } from '@home/components/profile/queue/tenant-profile-queues.component';
|
import { TenantProfileQueuesComponent } from '@home/components/profile/queue/tenant-profile-queues.component';
|
||||||
import { QueueFormComponent } from '@home/components/queue/queue-form.component';
|
import { QueueFormComponent } from '@home/components/queue/queue-form.component';
|
||||||
@ -253,6 +254,7 @@ import { WidgetSettingsComponent } from '@home/components/widget/widget-settings
|
|||||||
AlarmScheduleInfoComponent,
|
AlarmScheduleInfoComponent,
|
||||||
DeviceProfileProvisionConfigurationComponent,
|
DeviceProfileProvisionConfigurationComponent,
|
||||||
AlarmScheduleComponent,
|
AlarmScheduleComponent,
|
||||||
|
AlarmDynamicValue,
|
||||||
AlarmDurationPredicateValueComponent,
|
AlarmDurationPredicateValueComponent,
|
||||||
DeviceWizardDialogComponent,
|
DeviceWizardDialogComponent,
|
||||||
AlarmScheduleDialogComponent,
|
AlarmScheduleDialogComponent,
|
||||||
@ -370,11 +372,11 @@ import { WidgetSettingsComponent } from '@home/components/widget/widget-settings
|
|||||||
DeviceWizardDialogComponent,
|
DeviceWizardDialogComponent,
|
||||||
AlarmScheduleInfoComponent,
|
AlarmScheduleInfoComponent,
|
||||||
AlarmScheduleComponent,
|
AlarmScheduleComponent,
|
||||||
|
AlarmDynamicValue,
|
||||||
AlarmScheduleDialogComponent,
|
AlarmScheduleDialogComponent,
|
||||||
AlarmDurationPredicateValueComponent,
|
AlarmDurationPredicateValueComponent,
|
||||||
EditAlarmDetailsDialogComponent,
|
EditAlarmDetailsDialogComponent,
|
||||||
DeviceProfileProvisionConfigurationComponent,
|
DeviceProfileProvisionConfigurationComponent,
|
||||||
AlarmScheduleComponent,
|
|
||||||
SmsProviderConfigurationComponent,
|
SmsProviderConfigurationComponent,
|
||||||
AwsSnsProviderConfigurationComponent,
|
AwsSnsProviderConfigurationComponent,
|
||||||
SmppSmsProviderConfigurationComponent,
|
SmppSmsProviderConfigurationComponent,
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
<!--
|
||||||
|
|
||||||
|
Copyright © 2016-2022 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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<mat-expansion-panel [formGroup] = "dynamicValue" class="device-profile-alarm" style = "margin-bottom: 26px;" fxFlex>
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
|
||||||
|
<mat-panel-title>
|
||||||
|
<div fxLayout="row" fxFlex fxLayoutAlign="start center">
|
||||||
|
{{'filter.dynamic-value' | translate}}
|
||||||
|
</div>
|
||||||
|
</mat-panel-title>
|
||||||
|
<span fxFlex></span>
|
||||||
|
</div>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
<ng-template matExpansionPanelContent>
|
||||||
|
<div fxFlex fxLayout="column">
|
||||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
|
||||||
|
<div fxFlex="40" fxLayout="column">
|
||||||
|
<mat-form-field floatLabel="always" hideRequiredMarker class="mat-block">
|
||||||
|
<mat-label></mat-label>
|
||||||
|
<mat-select formControlName="sourceType" placeholder="{{'filter.dynamic-source-type' | translate}}">
|
||||||
|
<mat-option [value]="null">
|
||||||
|
{{'filter.no-dynamic-value' | translate}}
|
||||||
|
</mat-option>
|
||||||
|
<mat-option *ngFor="let sourceType of dynamicValueSourceTypes" [value]="sourceType">
|
||||||
|
{{dynamicValueSourceTypeTranslations.get(sourceType) | translate}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div fxFlex fxLayout="column">
|
||||||
|
<mat-form-field floatLabel="always" hideRequiredMarker class="mat-block source-attribute">
|
||||||
|
<mat-label></mat-label>
|
||||||
|
<input matInput formControlName="sourceAttribute" placeholder="{{'filter.source-attribute' | translate}}">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div [tb-help-popup]="helpId"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</mat-expansion-panel>
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
///
|
||||||
|
/// Copyright © 2016-2022 The Thingsboard Authors
|
||||||
|
///
|
||||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
/// you may not use this file except in compliance with the License.
|
||||||
|
/// You may obtain a copy of the License at
|
||||||
|
///
|
||||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
///
|
||||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
/// See the License for the specific language governing permissions and
|
||||||
|
/// limitations under the License.
|
||||||
|
///
|
||||||
|
|
||||||
|
import { Component, forwardRef, Input, OnInit } from '@angular/core';
|
||||||
|
import {
|
||||||
|
ControlValueAccessor,
|
||||||
|
FormBuilder,
|
||||||
|
FormGroup,
|
||||||
|
NG_VALUE_ACCESSOR,
|
||||||
|
} from '@angular/forms';
|
||||||
|
import {
|
||||||
|
DynamicValueSourceType,
|
||||||
|
dynamicValueSourceTypeTranslationMap,
|
||||||
|
getDynamicSourcesForAllowUser
|
||||||
|
} from '@shared/models/query/query.models';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tb-alarm-dynamic-value',
|
||||||
|
templateUrl: './alarm-dynamic-value.component.html',
|
||||||
|
providers: [{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => AlarmDynamicValue),
|
||||||
|
multi: true
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
export class AlarmDynamicValue implements ControlValueAccessor, OnInit{
|
||||||
|
public dynamicValue: FormGroup;
|
||||||
|
public dynamicValueSourceTypes: DynamicValueSourceType[] = getDynamicSourcesForAllowUser(false);
|
||||||
|
public dynamicValueSourceTypeTranslations = dynamicValueSourceTypeTranslationMap;
|
||||||
|
private propagateChange = (v: any) => { };
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
helpId: string;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
disabled: boolean;
|
||||||
|
|
||||||
|
constructor(private fb: FormBuilder) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.dynamicValue = this.fb.group({
|
||||||
|
sourceType: [null, []],
|
||||||
|
sourceAttribute: [null]
|
||||||
|
})
|
||||||
|
|
||||||
|
this.dynamicValue.get('sourceType').valueChanges.subscribe(
|
||||||
|
(sourceType) => {
|
||||||
|
if (!sourceType) {
|
||||||
|
this.dynamicValue.get('sourceAttribute').patchValue(null, {emitEvent: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.dynamicValue.valueChanges.subscribe(() => {
|
||||||
|
this.updateModel();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnChange(fn: any): void {
|
||||||
|
this.propagateChange = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnTouched(fn: any): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
writeValue(dynamicValue: {sourceType: string, sourceAttribute: string}): void {
|
||||||
|
if(dynamicValue) {
|
||||||
|
this.dynamicValue.patchValue(dynamicValue, {emitEvent: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisabledState(isDisabled: boolean): void {
|
||||||
|
this.disabled = isDisabled;
|
||||||
|
if (this.disabled) {
|
||||||
|
this.dynamicValue.disable({emitEvent: false});
|
||||||
|
} else {
|
||||||
|
this.dynamicValue.enable({emitEvent: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateModel() {
|
||||||
|
this.propagateChange(this.dynamicValue.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -34,6 +34,7 @@
|
|||||||
formControlName="timezone">
|
formControlName="timezone">
|
||||||
</tb-timezone-select>
|
</tb-timezone-select>
|
||||||
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME">
|
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME">
|
||||||
|
<tb-alarm-dynamic-value formControlName = 'dynamicValue' helpId = 'device-profile/alarm_specific_schedule_format'></tb-alarm-dynamic-value>
|
||||||
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
|
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
|
||||||
<div fxLayout="column" fxLayout.gt-md="row" fxLayoutGap="16px">
|
<div fxLayout="column" fxLayout.gt-md="row" fxLayoutGap="16px">
|
||||||
<div fxLayout="row" fxLayoutGap="16px">
|
<div fxLayout="row" fxLayoutGap="16px">
|
||||||
@ -73,8 +74,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM">
|
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM">
|
||||||
|
<tb-alarm-dynamic-value formControlName = 'dynamicValue' helpId = 'device-profile/alarm_сustom_schedule_format'></tb-alarm-dynamic-value>
|
||||||
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
|
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
|
||||||
|
|
||||||
<div *ngFor="let day of allDays" fxLayout="column" formArrayName="items" fxLayoutGap="1em">
|
<div *ngFor="let day of allDays" fxLayout="column" formArrayName="items" fxLayoutGap="1em">
|
||||||
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" [formGroupName]="''+day" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
|
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" [formGroupName]="''+day" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
|
||||||
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, day)">
|
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, day)">
|
||||||
|
|||||||
@ -64,7 +64,6 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
|
|||||||
alarmScheduleTypes = Object.keys(AlarmScheduleType);
|
alarmScheduleTypes = Object.keys(AlarmScheduleType);
|
||||||
alarmScheduleType = AlarmScheduleType;
|
alarmScheduleType = AlarmScheduleType;
|
||||||
alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap;
|
alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap;
|
||||||
|
|
||||||
dayOfWeekTranslationsArray = dayOfWeekTranslations;
|
dayOfWeekTranslationsArray = dayOfWeekTranslations;
|
||||||
|
|
||||||
allDays = Array(7).fill(0).map((x, i) => i);
|
allDays = Array(7).fill(0).map((x, i) => i);
|
||||||
@ -91,8 +90,10 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
|
|||||||
daysOfWeek: this.fb.array(new Array(7).fill(false), this.validateDayOfWeeks),
|
daysOfWeek: this.fb.array(new Array(7).fill(false), this.validateDayOfWeeks),
|
||||||
startsOn: [0, Validators.required],
|
startsOn: [0, Validators.required],
|
||||||
endsOn: [0, Validators.required],
|
endsOn: [0, Validators.required],
|
||||||
items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems)
|
items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems),
|
||||||
|
dynamicValue: [null]
|
||||||
});
|
});
|
||||||
|
|
||||||
this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => {
|
this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => {
|
||||||
const defaultTimezone = getDefaultTimezone();
|
const defaultTimezone = getDefaultTimezone();
|
||||||
this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: defaultTimezone}, {emitEvent: false});
|
this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: defaultTimezone}, {emitEvent: false});
|
||||||
@ -158,7 +159,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
|
|||||||
timezone: this.modelValue.timezone,
|
timezone: this.modelValue.timezone,
|
||||||
daysOfWeek,
|
daysOfWeek,
|
||||||
startsOn: utcTimestampToTimeOfDay(this.modelValue.startsOn),
|
startsOn: utcTimestampToTimeOfDay(this.modelValue.startsOn),
|
||||||
endsOn: utcTimestampToTimeOfDay(this.modelValue.endsOn)
|
endsOn: utcTimestampToTimeOfDay(this.modelValue.endsOn),
|
||||||
|
dynamicValue: this.modelValue.dynamicValue
|
||||||
}, {emitEvent: false});
|
}, {emitEvent: false});
|
||||||
break;
|
break;
|
||||||
case AlarmScheduleType.CUSTOM:
|
case AlarmScheduleType.CUSTOM:
|
||||||
@ -177,7 +179,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
|
|||||||
this.alarmScheduleForm.patchValue({
|
this.alarmScheduleForm.patchValue({
|
||||||
type: this.modelValue.type,
|
type: this.modelValue.type,
|
||||||
timezone: this.modelValue.timezone,
|
timezone: this.modelValue.timezone,
|
||||||
items: alarmDays
|
items: alarmDays,
|
||||||
|
dynamicValue: this.modelValue.dynamicValue
|
||||||
}, {emitEvent: false});
|
}, {emitEvent: false});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -480,6 +480,10 @@ export const AlarmScheduleTypeTranslationMap = new Map<AlarmScheduleType, string
|
|||||||
);
|
);
|
||||||
|
|
||||||
export interface AlarmSchedule{
|
export interface AlarmSchedule{
|
||||||
|
dynamicValue?: {
|
||||||
|
sourceAttribute: string,
|
||||||
|
sourceType: string;
|
||||||
|
};
|
||||||
type: AlarmScheduleType;
|
type: AlarmScheduleType;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
daysOfWeek?: number[];
|
daysOfWeek?: number[];
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
#### Specific schedule format
|
||||||
|
|
||||||
|
An attribute with a dynamic value for a specific schedule format must have JSON in the following format:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"daysOfWeek": [
|
||||||
|
2,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"endsOn": 0,
|
||||||
|
"startsOn": 0,
|
||||||
|
"timezone": "Europe/Kiev"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>timezone:</b> this value is used to designate the timezone you are using.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>daysOfWeek:</b> this value is used to designate the days in numerical representation (Monday - 1, Tuesday 2, etc.) on which the schedule will be active.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>startsOn:</b> this value is used to designate the timestamp in milliseconds, from which the schedule will be active for the designated days.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>endsOn:</b> this value is used to designate the timestamp in milliseconds until which the schedule will be active for the specified days.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
When <b>startsOn</b> and <b>endsOn</b> equals 0 it's means that the schedule will be active the whole day.
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
#### Custom schedule format
|
||||||
|
|
||||||
|
An attribute with a dynamic value for a custom schedule format must have JSON in the following format:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"timezone": "Europe/Kiev",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"dayOfWeek": 1,
|
||||||
|
"enabled": true,
|
||||||
|
"endsOn": 0,
|
||||||
|
"startsOn": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dayOfWeek": 2,
|
||||||
|
"enabled": true,
|
||||||
|
"endsOn": 0,
|
||||||
|
"startsOn": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dayOfWeek": 3,
|
||||||
|
"enabled": true,
|
||||||
|
"endsOn": 0,
|
||||||
|
"startsOn": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dayOfWeek": 4,
|
||||||
|
"enabled": true,
|
||||||
|
"endsOn": 0,
|
||||||
|
"startsOn": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dayOfWeek": 5,
|
||||||
|
"enabled": true,
|
||||||
|
"endsOn": 0,
|
||||||
|
"startsOn": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dayOfWeek": 6,
|
||||||
|
"enabled": true,
|
||||||
|
"endsOn": 0,
|
||||||
|
"startsOn": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dayOfWeek": 7,
|
||||||
|
"enabled": true,
|
||||||
|
"endsOn": 0,
|
||||||
|
"startsOn": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>timezone:</b> this value is used to designate the timezone you are using.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>items:</b> the array of values representing the days on which the schedule will be active.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
One array item contains such fields:
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>dayOfWeek:</b> this value is used to designate the specified day in numerical representation (Monday - 1, Tuesday 2, etc.) on which the schedule will be active.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>enabled:</b> this <code>boolean</code> value, used to designate that the specified day in the schedule will be enabled.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>startsOn:</b> this value is used to designate the timestamp in milliseconds, from which the schedule will be active for the designated day.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>endsOn:</b> this value is used to designate the timestamp in milliseconds until which the schedule will be active for the specified day.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
When <b>startsOn</b> and <b>endsOn</b> equals 0 it's means that the schedule will be active the whole day.
|
||||||
@ -2246,6 +2246,7 @@
|
|||||||
"current-device": "Current device",
|
"current-device": "Current device",
|
||||||
"default-value": "Default value",
|
"default-value": "Default value",
|
||||||
"dynamic-source-type": "Dynamic source type",
|
"dynamic-source-type": "Dynamic source type",
|
||||||
|
"dynamic-value": "Dynamic value",
|
||||||
"no-dynamic-value": "No dynamic value",
|
"no-dynamic-value": "No dynamic value",
|
||||||
"source-attribute": "Source attribute",
|
"source-attribute": "Source attribute",
|
||||||
"switch-to-dynamic-value": "Switch to dynamic value",
|
"switch-to-dynamic-value": "Switch to dynamic value",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user