Merge pull request #5818 from desoliture1/alarmScheduleDynamicValues

[3.4] Add support for dynamic values for schedules in alarm rules
This commit is contained in:
Igor Kulikov 2022-06-10 17:46:32 +03:00 committed by GitHub
commit 44749f1306
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 526 additions and 14 deletions

View File

@ -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();
} }

View File

@ -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;
}
} }

View File

@ -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;

View File

@ -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;

View File

@ -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);
} }

View File

@ -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;

View File

@ -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) {

View File

@ -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 {

View File

@ -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,

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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)">

View File

@ -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;

View File

@ -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[];

View File

@ -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.

View File

@ -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.

View File

@ -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",