Browse Source

Merge branch 'feature/device-provision-3.2-onlyProfileVersion' of https://github.com/zbeacon/thingsboard into feature/device-provision-3.2-onlyProfileVersion

pull/3518/head
zbeacon 6 years ago
parent
commit
16732dfaa9
  1. 6
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  2. 16
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  3. 5
      application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java
  4. 3
      application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java
  5. 2
      application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimJsonDeviceTest.java
  6. 5
      application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimProtoDeviceTest.java
  7. 3
      application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java
  8. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java
  9. 6
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java
  10. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java
  11. 4
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java
  12. 4
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java
  13. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java
  14. 3
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java
  15. 30
      common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java
  16. 364
      dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java
  17. 85
      dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java
  18. 1
      pom.xml
  19. 99
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java
  20. 13
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileAlarmState.java
  21. 15
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java
  22. 3
      tools/src/main/java/org/thingsboard/client/tools/MqttSslClient.java
  23. 1
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html
  24. 142
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html
  25. 29
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss
  26. 92
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts
  27. 2
      ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html
  28. 4
      ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss
  29. 24
      ui-ngx/src/app/shared/models/device.models.ts
  30. 12
      ui-ngx/src/assets/locale/locale.constant-en_US.json

6
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -90,7 +90,7 @@ public class AlarmController extends BaseController {
checkEntity(alarm.getId(), alarm, Resource.ALARM);
Alarm savedAlarm = checkNotNull(alarmService.createOrUpdateAlarm(alarm));
logEntityAction(savedAlarm.getId(), savedAlarm,
logEntityAction(savedAlarm.getOriginator(), savedAlarm,
getCurrentUser().getCustomerId(),
alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
return savedAlarm;
@ -126,7 +126,7 @@ public class AlarmController extends BaseController {
long ackTs = System.currentTimeMillis();
alarmService.ackAlarm(getCurrentUser().getTenantId(), alarmId, ackTs).get();
alarm.setAckTs(ackTs);
logEntityAction(alarmId, alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null);
logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null);
} catch (Exception e) {
throw handleException(e);
}
@ -143,7 +143,7 @@ public class AlarmController extends BaseController {
long clearTs = System.currentTimeMillis();
alarmService.clearAlarm(getCurrentUser().getTenantId(), alarmId, null, clearTs).get();
alarm.setClearTs(clearTs);
logEntityAction(alarmId, alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null);
logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null);
} catch (Exception e) {
throw handleException(e);
}

16
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -339,6 +339,19 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
} catch (Exception e) {
}
try {
conn.createStatement().execute("CREATE TABLE IF NOT EXISTS rule_node_state (" +
" id uuid NOT NULL CONSTRAINT rule_node_state_pkey PRIMARY KEY," +
" created_time bigint NOT NULL," +
" rule_node_id uuid NOT NULL," +
" entity_type varchar(32) NOT NULL," +
" entity_id uuid NOT NULL," +
" state_data varchar(16384) NOT NULL," +
" CONSTRAINT rule_node_state_unq_key UNIQUE (rule_node_id, entity_id)," +
" CONSTRAINT fk_rule_node_state_node_id FOREIGN KEY (rule_node_id) REFERENCES rule_node(id) ON DELETE CASCADE)");
} catch (Exception e) {
}
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.1.2", "schema_update_before.sql");
loadSql(schemaUpdateFile, conn);
@ -357,7 +370,8 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
List<EntitySubtype> deviceTypes = deviceService.findDeviceTypesByTenantId(tenant.getId()).get();
try {
deviceProfileService.createDefaultDeviceProfile(tenant.getId());
} catch (Exception e){}
} catch (Exception e) {
}
for (EntitySubtype deviceType : deviceTypes) {
try {
deviceProfileService.findOrCreateDeviceProfile(tenant.getId(), deviceType.getType());

5
application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java

@ -22,6 +22,7 @@ import org.apache.commons.lang3.RandomStringUtils;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@ -424,7 +425,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
assertNotNull(accessToken);
String clientId = MqttAsyncClient.generateClientId();
MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId);
MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(accessToken);
@ -466,7 +467,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
assertNotNull(accessToken);
String clientId = MqttAsyncClient.generateClientId();
MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId);
MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(accessToken);

3
application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java

@ -21,6 +21,7 @@ import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.junit.Assert;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.Device;
@ -144,7 +145,7 @@ public abstract class AbstractMqttIntegrationTest extends AbstractControllerTest
protected MqttAsyncClient getMqttAsyncClient(String accessToken) throws MqttException {
String clientId = MqttAsyncClient.generateClientId();
MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId);
MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(accessToken);

2
application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimJsonDeviceTest.java

@ -18,6 +18,7 @@ package org.thingsboard.server.mqtt.claim;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -51,6 +52,7 @@ public abstract class AbstractMqttClaimJsonDeviceTest extends AbstractMqttClaimD
}
@Test
@Ignore
public void testGatewayClaimingDeviceWithoutSecretAndDuration() throws Exception {
processTestGatewayClaimingDevice("Test claiming gateway device empty payload Json", true);
}

5
application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimProtoDeviceTest.java

@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.server.common.data.TransportPayloadType;
import org.thingsboard.server.gen.transport.TransportApiProtos;
@ -36,21 +37,25 @@ public abstract class AbstractMqttClaimProtoDeviceTest extends AbstractMqttClaim
public void afterTest() throws Exception { super.afterTest(); }
@Test
@Ignore
public void testClaimingDevice() throws Exception {
processTestClaimingDevice(false);
}
@Test
@Ignore
public void testClaimingDeviceWithoutSecretAndDuration() throws Exception {
processTestClaimingDevice(true);
}
@Test
@Ignore
public void testGatewayClaimingDevice() throws Exception {
processTestGatewayClaimingDevice("Test claiming gateway device Proto", false);
}
@Test
@Ignore
public void testGatewayClaimingDeviceWithoutSecretAndDuration() throws Exception {
processTestGatewayClaimingDevice("Test claiming gateway device empty payload Proto", true);
}

3
application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java

@ -22,6 +22,7 @@ import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -228,7 +229,7 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt
// @Test - Unstable
public void testMqttQoSLevel() throws Exception {
String clientId = MqttAsyncClient.generateClientId();
MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId);
MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(accessToken);

2
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data.device.profile;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@ -30,6 +31,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonSubTypes.Type(value = RepeatingAlarmConditionSpec.class, name = "REPEATING")})
public interface AlarmConditionSpec {
@JsonIgnore
AlarmConditionSpecType getType();
}

6
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java

@ -25,9 +25,9 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
include = JsonTypeInfo.As.PROPERTY,
property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = SimpleAlarmConditionSpec.class, name = "ANY_TIME"),
@JsonSubTypes.Type(value = DurationAlarmConditionSpec.class, name = "SPECIFIC_TIME"),
@JsonSubTypes.Type(value = RepeatingAlarmConditionSpec.class, name = "CUSTOM")})
@JsonSubTypes.Type(value = AnyTimeSchedule.class, name = "ANY_TIME"),
@JsonSubTypes.Type(value = SpecificTimeSchedule.class, name = "SPECIFIC_TIME"),
@JsonSubTypes.Type(value = CustomTimeSchedule.class, name = "CUSTOM")})
public interface AlarmSchedule {
AlarmScheduleType getType();

2
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java

@ -23,7 +23,7 @@ import java.util.List;
public class CustomTimeScheduleItem {
private boolean enabled;
private Integer dayOfWeek;
private int dayOfWeek;
private long startsOn;
private long endsOn;

4
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java

@ -15,11 +15,13 @@
*/
package org.thingsboard.server.common.data.device.profile;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.concurrent.TimeUnit;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DurationAlarmConditionSpec implements AlarmConditionSpec {
private TimeUnit unit;
@ -27,6 +29,6 @@ public class DurationAlarmConditionSpec implements AlarmConditionSpec {
@Override
public AlarmConditionSpecType getType() {
return AlarmConditionSpecType.SIMPLE;
return AlarmConditionSpecType.DURATION;
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java

@ -15,17 +15,19 @@
*/
package org.thingsboard.server.common.data.device.profile;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.concurrent.TimeUnit;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class RepeatingAlarmConditionSpec implements AlarmConditionSpec {
private int count;
@Override
public AlarmConditionSpecType getType() {
return AlarmConditionSpecType.SIMPLE;
return AlarmConditionSpecType.REPEATING;
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java

@ -15,9 +15,11 @@
*/
package org.thingsboard.server.common.data.device.profile;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class SimpleAlarmConditionSpec implements AlarmConditionSpec {
@Override
public AlarmConditionSpecType getType() {

3
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java

@ -18,12 +18,13 @@ package org.thingsboard.server.common.data.device.profile;
import lombok.Data;
import java.util.List;
import java.util.Set;
@Data
public class SpecificTimeSchedule implements AlarmSchedule {
private String timezone;
private List<Integer> daysOfWeek;
private Set<Integer> daysOfWeek;
private long startsOn;
private long endsOn;

30
common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.msg.tools;
import java.time.ZoneId;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class SchedulerUtils {
private static final ConcurrentMap<String, ZoneId> tzMap = new ConcurrentHashMap<>();
public static ZoneId getZoneId(String tz) {
return tzMap.computeIfAbsent(tz == null || tz.isEmpty() ? "UTC" : tz, ZoneId::of);
}
}

364
dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java

@ -0,0 +1,364 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.io.sstable;
import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.base.Objects;
import org.apache.cassandra.db.Directories;
import org.apache.cassandra.io.sstable.format.SSTableFormat;
import org.apache.cassandra.io.sstable.format.Version;
import org.apache.cassandra.io.sstable.metadata.IMetadataSerializer;
import org.apache.cassandra.io.sstable.metadata.LegacyMetadataSerializer;
import org.apache.cassandra.io.sstable.metadata.MetadataSerializer;
import org.apache.cassandra.utils.Pair;
import static org.apache.cassandra.io.sstable.Component.separator;
/**
* A SSTable is described by the keyspace and column family it contains data
* for, a generation (where higher generations contain more recent data) and
* an alphabetic version string.
*
* A descriptor can be marked as temporary, which influences generated filenames.
*/
public class Descriptor
{
public static String TMP_EXT = ".tmp";
/** canonicalized path to the directory where SSTable resides */
public final File directory;
/** version has the following format: <code>[a-z]+</code> */
public final Version version;
public final String ksname;
public final String cfname;
public final int generation;
public final SSTableFormat.Type formatType;
/** digest component - might be {@code null} for old, legacy sstables */
public final Component digestComponent;
private final int hashCode;
/**
* A descriptor that assumes CURRENT_VERSION.
*/
@VisibleForTesting
public Descriptor(File directory, String ksname, String cfname, int generation)
{
this(SSTableFormat.Type.current().info.getLatestVersion(), directory, ksname, cfname, generation, SSTableFormat.Type.current(), null);
}
/**
* Constructor for sstable writers only.
*/
public Descriptor(File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType)
{
this(formatType.info.getLatestVersion(), directory, ksname, cfname, generation, formatType, Component.digestFor(formatType.info.getLatestVersion().uncompressedChecksumType()));
}
@VisibleForTesting
public Descriptor(String version, File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType)
{
this(formatType.info.getVersion(version), directory, ksname, cfname, generation, formatType, Component.digestFor(formatType.info.getLatestVersion().uncompressedChecksumType()));
}
public Descriptor(Version version, File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType, Component digestComponent)
{
assert version != null && directory != null && ksname != null && cfname != null && formatType.info.getLatestVersion().getClass().equals(version.getClass());
this.version = version;
try
{
this.directory = directory.getCanonicalFile();
}
catch (IOException e)
{
throw new IOError(e);
}
this.ksname = ksname;
this.cfname = cfname;
this.generation = generation;
this.formatType = formatType;
this.digestComponent = digestComponent;
hashCode = Objects.hashCode(version, this.directory, generation, ksname, cfname, formatType);
}
public Descriptor withGeneration(int newGeneration)
{
return new Descriptor(version, directory, ksname, cfname, newGeneration, formatType, digestComponent);
}
public Descriptor withFormatType(SSTableFormat.Type newType)
{
return new Descriptor(newType.info.getLatestVersion(), directory, ksname, cfname, generation, newType, digestComponent);
}
public Descriptor withDigestComponent(Component newDigestComponent)
{
return new Descriptor(version, directory, ksname, cfname, generation, formatType, newDigestComponent);
}
public String tmpFilenameFor(Component component)
{
return filenameFor(component) + TMP_EXT;
}
public String filenameFor(Component component)
{
return baseFilename() + separator + component.name();
}
public String baseFilename()
{
StringBuilder buff = new StringBuilder();
buff.append(directory).append(File.separatorChar);
appendFileName(buff);
return buff.toString();
}
private void appendFileName(StringBuilder buff)
{
if (!version.hasNewFileName())
{
buff.append(ksname).append(separator);
buff.append(cfname).append(separator);
}
buff.append(version).append(separator);
buff.append(generation);
if (formatType != SSTableFormat.Type.LEGACY)
buff.append(separator).append(formatType.name);
}
public String relativeFilenameFor(Component component)
{
final StringBuilder buff = new StringBuilder();
appendFileName(buff);
buff.append(separator).append(component.name());
return buff.toString();
}
public SSTableFormat getFormat()
{
return formatType.info;
}
/** Return any temporary files found in the directory */
public List<File> getTemporaryFiles()
{
List<File> ret = new ArrayList<>();
File[] tmpFiles = directory.listFiles((dir, name) ->
name.endsWith(Descriptor.TMP_EXT));
for (File tmpFile : tmpFiles)
ret.add(tmpFile);
return ret;
}
/**
* Files obsoleted by CASSANDRA-7066 : temporary files and compactions_in_progress. We support
* versions 2.1 (ka) and 2.2 (la).
* Temporary files have tmp- or tmplink- at the beginning for 2.2 sstables or after ks-cf- for 2.1 sstables
*/
private final static String LEGACY_COMP_IN_PROG_REGEX_STR = "^compactions_in_progress(\\-[\\d,a-f]{32})?$";
private final static Pattern LEGACY_COMP_IN_PROG_REGEX = Pattern.compile(LEGACY_COMP_IN_PROG_REGEX_STR);
private final static String LEGACY_TMP_REGEX_STR = "^((.*)\\-(.*)\\-)?tmp(link)?\\-((?:l|k).)\\-(\\d)*\\-(.*)$";
private final static Pattern LEGACY_TMP_REGEX = Pattern.compile(LEGACY_TMP_REGEX_STR);
public static boolean isLegacyFile(File file)
{
if (file.isDirectory())
return file.getParentFile() != null &&
file.getParentFile().getName().equalsIgnoreCase("system") &&
LEGACY_COMP_IN_PROG_REGEX.matcher(file.getName()).matches();
else
return LEGACY_TMP_REGEX.matcher(file.getName()).matches();
}
public static boolean isValidFile(String fileName)
{
return fileName.endsWith(".db") && !LEGACY_TMP_REGEX.matcher(fileName).matches();
}
/**
* @see #fromFilename(File directory, String name)
* @param filename The SSTable filename
* @return Descriptor of the SSTable initialized from filename
*/
public static Descriptor fromFilename(String filename)
{
return fromFilename(filename, false);
}
public static Descriptor fromFilename(String filename, SSTableFormat.Type formatType)
{
return fromFilename(filename).withFormatType(formatType);
}
public static Descriptor fromFilename(String filename, boolean skipComponent)
{
File file = new File(filename).getAbsoluteFile();
return fromFilename(file.getParentFile(), file.getName(), skipComponent).left;
}
public static Pair<Descriptor, String> fromFilename(File directory, String name)
{
return fromFilename(directory, name, false);
}
/**
* Filename of the form is vary by version:
*
* <ul>
* <li>&lt;ksname&gt;-&lt;cfname&gt;-(tmp-)?&lt;version&gt;-&lt;gen&gt;-&lt;component&gt; for cassandra 2.0 and before</li>
* <li>(&lt;tmp marker&gt;-)?&lt;version&gt;-&lt;gen&gt;-&lt;component&gt; for cassandra 3.0 and later</li>
* </ul>
*
* If this is for SSTable of secondary index, directory should ends with index name for 2.1+.
*
* @param directory The directory of the SSTable files
* @param name The name of the SSTable file
* @param skipComponent true if the name param should not be parsed for a component tag
*
* @return A Descriptor for the SSTable, and the Component remainder.
*/
public static Pair<Descriptor, String> fromFilename(File directory, String name, boolean skipComponent)
{
File parentDirectory = directory != null ? directory : new File(".");
// tokenize the filename
StringTokenizer st = new StringTokenizer(name, String.valueOf(separator));
String nexttok;
// read tokens backwards to determine version
Deque<String> tokenStack = new ArrayDeque<>();
while (st.hasMoreTokens())
{
tokenStack.push(st.nextToken());
}
// component suffix
String component = skipComponent ? null : tokenStack.pop();
nexttok = tokenStack.pop();
// generation OR format type
SSTableFormat.Type fmt = SSTableFormat.Type.LEGACY;
if (!CharMatcher.digit().matchesAllOf(nexttok))
{
fmt = SSTableFormat.Type.validate(nexttok);
nexttok = tokenStack.pop();
}
// generation
int generation = Integer.parseInt(nexttok);
// version
nexttok = tokenStack.pop();
if (!Version.validate(nexttok))
throw new UnsupportedOperationException("SSTable " + name + " is too old to open. Upgrade to 2.0 first, and run upgradesstables");
Version version = fmt.info.getVersion(nexttok);
// ks/cf names
String ksname, cfname;
if (version.hasNewFileName())
{
// for 2.1+ read ks and cf names from directory
File cfDirectory = parentDirectory;
// check if this is secondary index
String indexName = "";
if (cfDirectory.getName().startsWith(Directories.SECONDARY_INDEX_NAME_SEPARATOR))
{
indexName = cfDirectory.getName();
cfDirectory = cfDirectory.getParentFile();
}
if (cfDirectory.getName().equals(Directories.BACKUPS_SUBDIR))
{
cfDirectory = cfDirectory.getParentFile();
}
else if (cfDirectory.getParentFile().getName().equals(Directories.SNAPSHOT_SUBDIR))
{
cfDirectory = cfDirectory.getParentFile().getParentFile();
}
cfname = cfDirectory.getName().split("-")[0] + indexName;
ksname = cfDirectory.getParentFile().getName();
}
else
{
cfname = tokenStack.pop();
ksname = tokenStack.pop();
}
assert tokenStack.isEmpty() : "Invalid file name " + name + " in " + directory;
return Pair.create(new Descriptor(version, parentDirectory, ksname, cfname, generation, fmt,
// _assume_ version from version
Component.digestFor(version.uncompressedChecksumType())),
component);
}
public IMetadataSerializer getMetadataSerializer()
{
if (version.hasNewStatsFile())
return new MetadataSerializer();
else
return new LegacyMetadataSerializer();
}
/**
* @return true if the current Cassandra version can read the given sstable version
*/
public boolean isCompatible()
{
return version.isCompatible();
}
@Override
public String toString()
{
return baseFilename();
}
@Override
public boolean equals(Object o)
{
if (o == this)
return true;
if (!(o instanceof Descriptor))
return false;
Descriptor that = (Descriptor)o;
return that.directory.equals(this.directory)
&& that.generation == this.generation
&& that.ksname.equals(this.ksname)
&& that.cfname.equals(this.cfname)
&& that.formatType == this.formatType;
}
@Override
public int hashCode()
{
return hashCode;
}
}

85
dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java

@ -0,0 +1,85 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.io.sstable.format;
import com.google.common.base.CharMatcher;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.db.RowIndexEntry;
import org.apache.cassandra.db.SerializationHeader;
import org.apache.cassandra.io.sstable.format.big.BigFormat;
/**
* Provides the accessors to data on disk.
*/
public interface SSTableFormat
{
static boolean enableSSTableDevelopmentTestMode = Boolean.getBoolean("cassandra.test.sstableformatdevelopment");
Version getLatestVersion();
Version getVersion(String version);
SSTableWriter.Factory getWriterFactory();
SSTableReader.Factory getReaderFactory();
RowIndexEntry.IndexSerializer<?> getIndexSerializer(CFMetaData cfm, Version version, SerializationHeader header);
public static enum Type
{
//Used internally to refer to files with no
//format flag in the filename
LEGACY("big", BigFormat.instance),
//The original sstable format
BIG("big", BigFormat.instance);
public final SSTableFormat info;
public final String name;
public static Type current()
{
return BIG;
}
private Type(String name, SSTableFormat info)
{
//Since format comes right after generation
//we disallow formats with numeric names
// We have removed this check for compatibility with the embedded cassandra used for tests.
assert !CharMatcher.digit().matchesAllOf(name);
this.name = name;
this.info = info;
}
public static Type validate(String name)
{
for (Type valid : Type.values())
{
//This is used internally for old sstables
if (valid == LEGACY)
continue;
if (valid.name.equalsIgnoreCase(name))
return valid;
}
throw new IllegalArgumentException("No Type constant " + name);
}
}
}

1
pom.xml

@ -728,6 +728,7 @@
<exclude>ui/**</exclude>
<exclude>src/browserslist</exclude>
<exclude>**/*.raw</exclude>
<exclude>**/apache/cassandra/io/**</exclude>
</excludes>
<mapping>
<proto>JAVADOC_STYLE</proto>

99
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java

@ -21,15 +21,24 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.device.profile.AlarmCondition;
import org.thingsboard.server.common.data.device.profile.AlarmConditionSpec;
import org.thingsboard.server.common.data.device.profile.AlarmRule;
import org.thingsboard.server.common.data.device.profile.CustomTimeSchedule;
import org.thingsboard.server.common.data.device.profile.CustomTimeScheduleItem;
import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec;
import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec;
import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec;
import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.KeyFilterPredicate;
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.query.StringFilterPredicate;
import org.thingsboard.server.common.msg.tools.SchedulerUtils;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar;
@Data
public class AlarmRuleState {
@ -85,36 +94,91 @@ public class AlarmRuleState {
}
public boolean eval(DeviceDataSnapshot data) {
boolean active = isActive(data.getTs());
switch (spec.getType()) {
case SIMPLE:
return eval(alarmRule.getCondition(), data);
return active && eval(alarmRule.getCondition(), data);
case DURATION:
return evalDuration(data);
return evalDuration(data, active);
case REPEATING:
return evalRepeating(data);
return evalRepeating(data, active);
default:
return false;
}
}
private boolean evalRepeating(DeviceDataSnapshot data) {
boolean eval = eval(alarmRule.getCondition(), data);
if (eval) {
private boolean isActive(long eventTs) {
if (eventTs == 0L) {
eventTs = System.currentTimeMillis();
}
if (alarmRule.getSchedule() == null) {
return true;
}
switch (alarmRule.getSchedule().getType()) {
case ANY_TIME:
return true;
case SPECIFIC_TIME:
return isActiveSpecific((SpecificTimeSchedule) alarmRule.getSchedule(), eventTs);
case CUSTOM:
return isActiveCustom((CustomTimeSchedule) alarmRule.getSchedule(), eventTs);
default:
throw new RuntimeException("Unsupported schedule type: " + alarmRule.getSchedule().getType());
}
}
private boolean isActiveSpecific(SpecificTimeSchedule schedule, long eventTs) {
ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone());
ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId);
if (schedule.getDaysOfWeek().size() != 7) {
int dayOfWeek = zdt.getDayOfWeek().getValue();
if (!schedule.getDaysOfWeek().contains(dayOfWeek)) {
return false;
}
}
long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli();
long msFromStartOfDay = eventTs - startOfDay;
return schedule.getStartsOn() <= msFromStartOfDay && schedule.getEndsOn() > msFromStartOfDay;
}
private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) {
ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone());
ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId);
int dayOfWeek = zdt.toLocalDate().getDayOfWeek().getValue();
for (CustomTimeScheduleItem item : schedule.getItems()) {
if (item.getDayOfWeek() == dayOfWeek) {
if (item.isEnabled()) {
long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli();
long msFromStartOfDay = eventTs - startOfDay;
return item.getStartsOn() <= msFromStartOfDay && item.getEndsOn() > msFromStartOfDay;
} else {
return false;
}
}
}
return false;
}
public void clear() {
if (state.getEventCount() > 0 || state.getLastEventTs() > 0 || state.getDuration() > 0) {
state.setEventCount(0L);
state.setLastEventTs(0L);
state.setDuration(0L);
updateFlag = true;
}
}
private boolean evalRepeating(DeviceDataSnapshot data, boolean active) {
if (active && eval(alarmRule.getCondition(), data)) {
state.setEventCount(state.getEventCount() + 1);
updateFlag = true;
return state.getEventCount() > requiredRepeats;
return state.getEventCount() >= requiredRepeats;
} else {
if (state.getEventCount() > 0) {
state.setEventCount(0L);
updateFlag = true;
}
return false;
}
}
private boolean evalDuration(DeviceDataSnapshot data) {
boolean eval = eval(alarmRule.getCondition(), data);
if (eval) {
private boolean evalDuration(DeviceDataSnapshot data, boolean active) {
if (active && eval(alarmRule.getCondition(), data)) {
if (state.getLastEventTs() > 0) {
if (data.getTs() > state.getLastEventTs()) {
state.setDuration(state.getDuration() + (data.getTs() - state.getLastEventTs()));
@ -128,11 +192,6 @@ public class AlarmRuleState {
}
return state.getDuration() > requiredDurationInMs;
} else {
if (state.getLastEventTs() > 0 || state.getDuration() > 0) {
state.setLastEventTs(0L);
state.setDuration(0L);
updateFlag = true;
}
return false;
}
}
@ -145,7 +204,7 @@ public class AlarmRuleState {
case DURATION:
if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && ts > state.getLastEventTs()) {
long duration = state.getDuration() + (ts - state.getLastEventTs());
return duration > requiredDurationInMs;
return duration > requiredDurationInMs && isActive(ts);
}
default:
return false;

13
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceProfileAlarmState.java

@ -53,7 +53,6 @@ class DeviceProfileAlarmState {
public DeviceProfileAlarmState(EntityId originator, DeviceProfileAlarm alarmDefinition, PersistedAlarmState alarmState) {
this.originator = originator;
this.updateState(alarmDefinition, alarmState);
}
public boolean process(TbContext ctx, TbMsg msg, DeviceDataSnapshot data) throws ExecutionException, InterruptedException {
@ -179,5 +178,15 @@ class DeviceProfileAlarmState {
}
}
public boolean processAlarmClear(TbContext ctx, Alarm alarmNf) {
boolean updated = false;
if (currentAlarm != null && currentAlarm.getId().equals(alarmNf.getId())) {
currentAlarm = null;
for (AlarmRuleState state : createRulesSortedBySeverityDesc) {
state.clear();
updated |= state.checkUpdate();
}
}
return updated;
}
}

15
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java

@ -24,6 +24,7 @@ import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -130,6 +131,8 @@ class DeviceState {
stateChanged = processAttributesUpdateNotification(ctx, msg);
} else if (msg.getType().equals(DataConstants.ATTRIBUTES_DELETED)) {
stateChanged = processAttributesDeleteNotification(ctx, msg);
} else if (msg.getType().equals(DataConstants.ALARM_CLEAR)) {
stateChanged = processAlarmClearNotification(ctx, msg);
} else {
ctx.tellSuccess(msg);
}
@ -139,6 +142,18 @@ class DeviceState {
}
}
private boolean processAlarmClearNotification(TbContext ctx, TbMsg msg) {
boolean stateChanged = false;
Alarm alarmNf = JacksonUtil.fromString(msg.getData(), Alarm.class);
for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) {
DeviceProfileAlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(),
a -> new DeviceProfileAlarmState(deviceId, alarm, getOrInitPersistedAlarmState(alarm)));
stateChanged |= alarmState.processAlarmClear(ctx, alarmNf);
}
ctx.tellSuccess(msg);
return stateChanged;
}
private boolean processAttributesUpdateNotification(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException {
Set<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData()));
String scope = msg.getMetaData().getValue("scope");

3
tools/src/main/java/org/thingsboard/client/tools/MqttSslClient.java

@ -25,6 +25,7 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import javax.net.ssl.*;
import java.io.File;
@ -71,7 +72,7 @@ public class MqttSslClient {
MqttConnectOptions options = new MqttConnectOptions();
options.setSocketFactory(sslContext.getSocketFactory());
MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, CLIENT_ID);
MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, CLIENT_ID, new MemoryPersistence());
client.connect(options);
Thread.sleep(3000);
MqttMessage message = new MqttMessage();

1
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html

@ -17,7 +17,6 @@
-->
<div fxLayout="column" fxFlex>
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<div class="tb-small" translate>device-profile.alarm-rule-condition</div>
<span fxFlex></span>
<a mat-button color="primary"
type="button"

142
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html

@ -16,70 +16,90 @@
-->
<div fxLayout="column" [formGroup]="alarmRuleFormGroup">
<div formGroupName="condition" fxLayout="row" fxLayoutGap="8px" fxFlex>
<tb-alarm-rule-condition fxFlex
formControlName="condition">
</tb-alarm-rule-condition>
<div fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<div class="tb-small" translate>device-profile.condition-duration</div>
<span fxFlex></span>
<mat-slide-toggle [disabled]="disabled"
color="primary"
[ngModelOptions]="{standalone: true}"
(ngModelChange)="enableDurationChanged($event)"
[ngModel]="enableDuration">
</mat-slide-toggle>
</div>
<div class="tb-condition-duration" fxFlex fxLayout="row" fxLayoutGap="8px">
<span style="min-width: 250px;" *ngIf="!enableDuration"></span>
<div style="min-width: 250px;" fxLayout="row" fxLayoutGap="8px" *ngIf="enableDuration">
<mat-form-field class="mat-block duration-value-field" hideRequiredMarker floatLabel="always">
<mat-label></mat-label>
<input type="number"
required
step="1"
min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
formControlName="durationValue">
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('required')">
{{ 'device-profile.condition-duration-value-required' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('min')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('max')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block duration-unit-field" hideRequiredMarker floatLabel="always">
<mat-label></mat-label>
<mat-select formControlName="durationUnit"
required
placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
<mat-tab-group>
<mat-tab label="{{ 'device-profile.condition' | translate }}" formGroupName="condition">
<tb-alarm-rule-condition fxFlex class="row"
formControlName="condition">
</tb-alarm-rule-condition>
<section class="row">
<div formGroupName="spec">
<mat-form-field class="mat-block" hideRequiredMarker>
<mat-label translate>device-profile.condition-type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationUnit').hasError('required')">
{{ 'device-profile.condition-duration-time-unit-required' | translate }}
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.type').hasError('required')">
{{ 'device-profile.condition-type-required' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutGap="8px" *ngIf="alarmRuleFormGroup.get('condition.spec.type').value == AlarmConditionType.DURATION">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<input type="number" required
step="1" min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
formControlName="value">
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('required')">
{{ 'device-profile.condition-duration-value-required' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('min')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('max')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('pattern')">
{{ 'device-profile.condition-duration-value-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<mat-select formControlName="unit"
required
placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.unit').hasError('required')">
{{ 'device-profile.condition-duration-time-unit-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="8px" *ngIf="alarmRuleFormGroup.get('condition.spec.type').value == AlarmConditionType.REPEATING">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<input type="number" required
step="1" min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-repeating-value' | translate }}"
formControlName="count">
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('required')">
{{ 'device-profile.condition-repeating-value-required' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('min')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('max')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('pattern')">
{{ 'device-profile.condition-repeating-value-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
</div>
</div>
</div>
<mat-expansion-panel class="advanced-settings" [expanded]="false">
<mat-expansion-panel-header>
<mat-panel-title>
<div fxFlex fxLayout="row" fxLayoutAlign="end center">
<div class="tb-small" translate>device-profile.alarm-rule-details</div>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.alarm-details</mat-label>
<textarea matInput formControlName="alarmDetails" rows="5"></textarea>
</mat-form-field>
</mat-expansion-panel>
</section>
</mat-tab>
<mat-tab label="{{ 'device-profile.schedule' | translate }}">
<div class="row">{{ 'device-profile.schedule' | translate }}</div>
</mat-tab>
<mat-tab label="{{ 'device-profile.alarm-rule-details' | translate }}">
<mat-form-field class="mat-block row">
<mat-label translate>device-profile.alarm-details</mat-label>
<textarea matInput formControlName="alarmDetails" rows="5"></textarea>
</mat-form-field>
</mat-tab>
</mat-tab-group>
</div>

29
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss

@ -14,33 +14,8 @@
* limitations under the License.
*/
:host {
.tb-condition-duration {
padding: 8px;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
}
.mat-expansion-panel.advanced-settings {
box-shadow: none;
border: none;
padding: 0;
}
}
:host ::ng-deep {
.mat-expansion-panel.advanced-settings {
.mat-expansion-panel-body {
padding: 0;
}
}
.mat-form-field.duration-value-field {
.mat-form-field-infix {
width: 120px;
}
}
.mat-form-field.duration-unit-field {
.mat-form-field-infix {
width: 120px;
}
.row {
margin-top: 1em;
}
}

92
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, forwardRef, Input, NgZone, OnInit } from '@angular/core';
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
@ -25,9 +25,9 @@ import {
Validator,
Validators
} from '@angular/forms';
import { AlarmRule } from '@shared/models/device.models';
import { AlarmConditionType, AlarmConditionTypeTranslationMap, AlarmRule } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog';
import { TimeUnit, timeUnitTranslationMap } from '../../../../../shared/models/time/time.models';
import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
@Component({
@ -51,6 +51,9 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
timeUnits = Object.keys(TimeUnit);
timeUnitTranslations = timeUnitTranslationMap;
alarmConditionTypes = Object.keys(AlarmConditionType);
AlarmConditionType = AlarmConditionType;
alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap;
@Input()
disabled: boolean;
@ -64,8 +67,6 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
this.requiredValue = coerceBooleanProperty(value);
}
enableDuration = false;
private modelValue: AlarmRule;
alarmRuleFormGroup: FormGroup;
@ -87,11 +88,18 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
this.alarmRuleFormGroup = this.fb.group({
condition: this.fb.group({
condition: [null, Validators.required],
durationUnit: [null],
durationValue: [null]
spec: this.fb.group({
type: [AlarmConditionType.SIMPLE, Validators.required],
unit: [{value: null, disable: true}, Validators.required],
value: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]],
count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]]
})
}, Validators.required),
alarmDetails: [null]
});
this.alarmRuleFormGroup.get('condition.spec.type').valueChanges.subscribe((type) => {
this.updateValidators(type, true, true);
});
this.alarmRuleFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
@ -108,9 +116,13 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
writeValue(value: AlarmRule): void {
this.modelValue = value;
this.enableDuration = value && !!value.condition.durationValue;
if (this.modelValue?.condition?.spec === null) {
this.modelValue.condition.spec = {
type: AlarmConditionType.SIMPLE
};
}
this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false});
this.updateValidators();
this.updateValidators(this.modelValue?.condition?.spec?.type);
}
public validate(c: FormControl) {
@ -121,31 +133,45 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
};
}
public enableDurationChanged(enableDuration) {
this.enableDuration = enableDuration;
this.updateValidators(true, true);
}
private updateValidators(resetDuration = false, emitEvent = false) {
if (this.enableDuration) {
this.alarmRuleFormGroup.get('condition').get('durationValue')
.setValidators([Validators.required, Validators.min(1), Validators.max(2147483647)]);
this.alarmRuleFormGroup.get('condition').get('durationUnit')
.setValidators([Validators.required]);
} else {
this.alarmRuleFormGroup.get('condition').get('durationValue')
.setValidators([]);
this.alarmRuleFormGroup.get('condition').get('durationUnit')
.setValidators([]);
if (resetDuration) {
this.alarmRuleFormGroup.get('condition').patchValue({
durationValue: null,
durationUnit: null
});
}
private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) {
switch (type) {
case AlarmConditionType.DURATION:
this.alarmRuleFormGroup.get('condition.spec.value').enable();
this.alarmRuleFormGroup.get('condition.spec.unit').enable();
this.alarmRuleFormGroup.get('condition.spec.count').disable();
if (resetDuration) {
this.alarmRuleFormGroup.get('condition.spec').patchValue({
count: null
});
}
break;
case AlarmConditionType.REPEATING:
this.alarmRuleFormGroup.get('condition.spec.count').enable();
this.alarmRuleFormGroup.get('condition.spec.value').disable();
this.alarmRuleFormGroup.get('condition.spec.unit').disable();
if (resetDuration) {
this.alarmRuleFormGroup.get('condition.spec').patchValue({
value: null,
unit: null
});
}
break;
case AlarmConditionType.SIMPLE:
this.alarmRuleFormGroup.get('condition.spec.value').disable();
this.alarmRuleFormGroup.get('condition.spec.unit').disable();
this.alarmRuleFormGroup.get('condition.spec.count').disable();
if (resetDuration) {
this.alarmRuleFormGroup.get('condition.spec').patchValue({
value: null,
unit: null,
count: null
});
}
break;
}
this.alarmRuleFormGroup.get('condition').get('durationValue').updateValueAndValidity({emitEvent});
this.alarmRuleFormGroup.get('condition').get('durationUnit').updateValueAndValidity({emitEvent});
this.alarmRuleFormGroup.get('condition.spec.value').updateValueAndValidity({emitEvent});
this.alarmRuleFormGroup.get('condition.spec.unit').updateValueAndValidity({emitEvent});
this.alarmRuleFormGroup.get('condition.spec.count').updateValueAndValidity({emitEvent});
}
private updateModel() {

2
ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html

@ -19,7 +19,7 @@
<div *ngFor="let createAlarmRuleControl of createAlarmRulesFormArray().controls; let $index = index;
last as isLast;" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px" style="padding-bottom: 8px;" [formGroup]="createAlarmRuleControl">
<div class="create-alarm-rule" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
<div class="create-alarm-rule" fxFlex fxLayout="column" fxLayoutGap="8px" fxLayoutAlign="start">
<mat-form-field class="severity mat-block" floatLabel="always" hideRequiredMarker>
<mat-label translate>alarm.severity</mat-label>
<mat-select formControlName="severity"

4
ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss

@ -19,10 +19,6 @@
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
padding: 8px;
.mat-form-field.severity {
border-right: 1px groove rgba(0, 0, 0, 0.25);
padding-right: 8px;
}
}
}

24
ui-ngx/src/app/shared/models/device.models.ts

@ -231,10 +231,30 @@ export function createDeviceTransportConfiguration(type: DeviceTransportType): D
return transportConfiguration;
}
export enum AlarmConditionType {
SIMPLE = 'SIMPLE',
DURATION = 'DURATION',
REPEATING = 'REPEATING'
}
export const AlarmConditionTypeTranslationMap = new Map<AlarmConditionType, string>(
[
[AlarmConditionType.SIMPLE, 'device-profile.condition-type-simple'],
[AlarmConditionType.DURATION, 'device-profile.condition-type-duration'],
[AlarmConditionType.REPEATING, 'device-profile.condition-type-repeating']
]
);
export interface AlarmConditionSpec{
type?: AlarmConditionType;
unit?: TimeUnit;
value?: number;
count?: number;
}
export interface AlarmCondition {
condition: Array<KeyFilter>;
durationUnit?: TimeUnit;
durationValue?: number;
spec?: AlarmConditionSpec;
}
export interface AlarmRule {

12
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -834,6 +834,7 @@
"condition-duration-value": "Duration value",
"condition-duration-time-unit": "Time unit",
"condition-duration-value-range": "Duration value should be in a range from 1 to 2147483647.",
"condition-duration-value-pattern": "Duration value should be integers.",
"condition-duration-value-required": "Duration value is required.",
"condition-duration-time-unit-required": "Time unit is required.",
"advanced-settings": "Advanced settings",
@ -855,6 +856,17 @@
"provision-device-key-required": "Provision device key is required.",
"provision-device-secret": "Provision device secret",
"provision-device-secret-required": "Provision device secret is required."
"condition": "Condition",
"condition-type": "Condition type",
"condition-type-simple": "Simple",
"condition-type-duration": "Duration",
"condition-type-repeating": "Repeating",
"condition-type-required": "Condition type is required.",
"condition-repeating-value": "Count of events",
"condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.",
"condition-repeating-value-pattern": "Count of events should be integers.",
"condition-repeating-value-required": "Count of events is required.",
"schedule": "Schedule"
},
"dialog": {
"close": "Close dialog"

Loading…
Cancel
Save