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.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import org.thingsboard.server.common.data.query.DynamicValue;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@ -34,4 +35,6 @@ public interface AlarmSchedule extends Serializable {
|
||||
|
||||
AlarmScheduleType getType();
|
||||
|
||||
DynamicValue<String> getDynamicValue();
|
||||
|
||||
}
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.thingsboard.server.common.data.device.profile;
|
||||
|
||||
import org.thingsboard.server.common.data.query.DynamicValue;
|
||||
|
||||
public class AnyTimeSchedule implements AlarmSchedule {
|
||||
|
||||
@Override
|
||||
@ -22,4 +24,9 @@ public class AnyTimeSchedule implements AlarmSchedule {
|
||||
return AlarmScheduleType.ANY_TIME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DynamicValue<String> getDynamicValue() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
package org.thingsboard.server.common.data.device.profile;
|
||||
|
||||
import lombok.Data;
|
||||
import org.thingsboard.server.common.data.query.DynamicValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -25,6 +26,8 @@ public class CustomTimeSchedule implements AlarmSchedule {
|
||||
private String timezone;
|
||||
private List<CustomTimeScheduleItem> items;
|
||||
|
||||
private DynamicValue<String> dynamicValue;
|
||||
|
||||
@Override
|
||||
public AlarmScheduleType getType() {
|
||||
return AlarmScheduleType.CUSTOM;
|
||||
|
||||
@ -16,8 +16,8 @@
|
||||
package org.thingsboard.server.common.data.device.profile;
|
||||
|
||||
import lombok.Data;
|
||||
import org.thingsboard.server.common.data.query.DynamicValue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Data
|
||||
@ -28,6 +28,8 @@ public class SpecificTimeSchedule implements AlarmSchedule {
|
||||
private long startsOn;
|
||||
private long endsOn;
|
||||
|
||||
private DynamicValue<String> dynamicValue;
|
||||
|
||||
@Override
|
||||
public AlarmScheduleType getType() {
|
||||
return AlarmScheduleType.SPECIFIC_TIME;
|
||||
|
||||
@ -575,6 +575,10 @@ public class JsonConverter {
|
||||
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) {
|
||||
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.AlarmRule;
|
||||
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.DurationAlarmConditionSpec;
|
||||
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.StringFilterPredicate;
|
||||
import org.thingsboard.server.common.msg.tools.SchedulerUtils;
|
||||
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
@ -115,7 +117,7 @@ class AlarmRuleState {
|
||||
}
|
||||
|
||||
public AlarmEvalResult eval(DataSnapshot data) {
|
||||
boolean active = isActive(data.getTs());
|
||||
boolean active = isActive(data, data.getTs());
|
||||
switch (spec.getType()) {
|
||||
case SIMPLE:
|
||||
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) {
|
||||
eventTs = System.currentTimeMillis();
|
||||
}
|
||||
@ -139,14 +141,28 @@ class AlarmRuleState {
|
||||
case ANY_TIME:
|
||||
return true;
|
||||
case SPECIFIC_TIME:
|
||||
return isActiveSpecific((SpecificTimeSchedule) alarmRule.getSchedule(), eventTs);
|
||||
return isActiveSpecific((SpecificTimeSchedule) getSchedule(data, alarmRule), eventTs);
|
||||
case CUSTOM:
|
||||
return isActiveCustom((CustomTimeSchedule) alarmRule.getSchedule(), eventTs);
|
||||
return isActiveCustom((CustomTimeSchedule) getSchedule(data, alarmRule), eventTs);
|
||||
default:
|
||||
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) {
|
||||
ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone());
|
||||
ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId);
|
||||
@ -156,7 +172,13 @@ class AlarmRuleState {
|
||||
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) {
|
||||
@ -166,7 +188,12 @@ class AlarmRuleState {
|
||||
for (CustomTimeScheduleItem item : schedule.getItems()) {
|
||||
if (item.getDayOfWeek() == dayOfWeek) {
|
||||
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 {
|
||||
return false;
|
||||
}
|
||||
@ -279,7 +306,7 @@ class AlarmRuleState {
|
||||
long requiredDurationInMs = resolveRequiredDurationInMs(dataSnapshot);
|
||||
if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && 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;
|
||||
} else {
|
||||
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.DurationAlarmConditionSpec;
|
||||
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.query.ComplexFilterPredicate;
|
||||
import org.thingsboard.server.common.data.query.DynamicValue;
|
||||
@ -77,6 +78,10 @@ class ProfileState {
|
||||
addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, ruleKeys);
|
||||
}
|
||||
addEntityKeysFromAlarmConditionSpec(alarmRule);
|
||||
AlarmSchedule schedule = alarmRule.getSchedule();
|
||||
if (schedule != null) {
|
||||
addScheduleDynamicValues(schedule);
|
||||
}
|
||||
}));
|
||||
if (alarm.getClearRule() != null) {
|
||||
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) {
|
||||
AlarmConditionSpec spec = alarmRule.getCondition().getSpec();
|
||||
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.DurationAlarmConditionSpec;
|
||||
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.DeviceId;
|
||||
import org.thingsboard.server.common.data.id.DeviceProfileId;
|
||||
@ -71,6 +73,7 @@ import java.math.RoundingMode;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Optional;
|
||||
import java.util.TreeMap;
|
||||
import java.util.UUID;
|
||||
@ -1086,6 +1089,179 @@ public class TbDeviceProfileNodeTest {
|
||||
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
|
||||
public void testCurrentCustomersAttributeForDynamicValue() throws Exception {
|
||||
|
||||
@ -148,6 +148,7 @@ import {
|
||||
HOME_COMPONENTS_MODULE_TOKEN
|
||||
} from '@home/components/tokens';
|
||||
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 { TenantProfileQueuesComponent } from '@home/components/profile/queue/tenant-profile-queues.component';
|
||||
import { QueueFormComponent } from '@home/components/queue/queue-form.component';
|
||||
@ -253,6 +254,7 @@ import { WidgetSettingsComponent } from '@home/components/widget/widget-settings
|
||||
AlarmScheduleInfoComponent,
|
||||
DeviceProfileProvisionConfigurationComponent,
|
||||
AlarmScheduleComponent,
|
||||
AlarmDynamicValue,
|
||||
AlarmDurationPredicateValueComponent,
|
||||
DeviceWizardDialogComponent,
|
||||
AlarmScheduleDialogComponent,
|
||||
@ -370,11 +372,11 @@ import { WidgetSettingsComponent } from '@home/components/widget/widget-settings
|
||||
DeviceWizardDialogComponent,
|
||||
AlarmScheduleInfoComponent,
|
||||
AlarmScheduleComponent,
|
||||
AlarmDynamicValue,
|
||||
AlarmScheduleDialogComponent,
|
||||
AlarmDurationPredicateValueComponent,
|
||||
EditAlarmDetailsDialogComponent,
|
||||
DeviceProfileProvisionConfigurationComponent,
|
||||
AlarmScheduleComponent,
|
||||
SmsProviderConfigurationComponent,
|
||||
AwsSnsProviderConfigurationComponent,
|
||||
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">
|
||||
</tb-timezone-select>
|
||||
<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 fxLayout="column" fxLayout.gt-md="row" fxLayoutGap="16px">
|
||||
<div fxLayout="row" fxLayoutGap="16px">
|
||||
@ -73,8 +74,8 @@
|
||||
</div>
|
||||
</section>
|
||||
<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 *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">
|
||||
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, day)">
|
||||
|
||||
@ -64,7 +64,6 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
|
||||
alarmScheduleTypes = Object.keys(AlarmScheduleType);
|
||||
alarmScheduleType = AlarmScheduleType;
|
||||
alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap;
|
||||
|
||||
dayOfWeekTranslationsArray = dayOfWeekTranslations;
|
||||
|
||||
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),
|
||||
startsOn: [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) => {
|
||||
const defaultTimezone = getDefaultTimezone();
|
||||
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,
|
||||
daysOfWeek,
|
||||
startsOn: utcTimestampToTimeOfDay(this.modelValue.startsOn),
|
||||
endsOn: utcTimestampToTimeOfDay(this.modelValue.endsOn)
|
||||
endsOn: utcTimestampToTimeOfDay(this.modelValue.endsOn),
|
||||
dynamicValue: this.modelValue.dynamicValue
|
||||
}, {emitEvent: false});
|
||||
break;
|
||||
case AlarmScheduleType.CUSTOM:
|
||||
@ -177,7 +179,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
|
||||
this.alarmScheduleForm.patchValue({
|
||||
type: this.modelValue.type,
|
||||
timezone: this.modelValue.timezone,
|
||||
items: alarmDays
|
||||
items: alarmDays,
|
||||
dynamicValue: this.modelValue.dynamicValue
|
||||
}, {emitEvent: false});
|
||||
}
|
||||
break;
|
||||
|
||||
@ -480,6 +480,10 @@ export const AlarmScheduleTypeTranslationMap = new Map<AlarmScheduleType, string
|
||||
);
|
||||
|
||||
export interface AlarmSchedule{
|
||||
dynamicValue?: {
|
||||
sourceAttribute: string,
|
||||
sourceType: string;
|
||||
};
|
||||
type: AlarmScheduleType;
|
||||
timezone?: string;
|
||||
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",
|
||||
"default-value": "Default value",
|
||||
"dynamic-source-type": "Dynamic source type",
|
||||
"dynamic-value": "Dynamic value",
|
||||
"no-dynamic-value": "No dynamic value",
|
||||
"source-attribute": "Source attribute",
|
||||
"switch-to-dynamic-value": "Switch to dynamic value",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user