diff --git a/.editorconfig b/.editorconfig index 276bcb40..d2b278bc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,6 +2,7 @@ root = true [*] charset = utf-8 +end_of_line = lf [*.java] indent_style = space diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index c70e4239..178a7114 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -19,7 +19,7 @@ jobs: path: ~/.m2 key: jetlinks-community-maven-repository - name: Build with Maven - run: mvn -B package && cd jetlinks-standalone && mvn docker:build + run: mvn -B package -Pbuild && cd jetlinks-standalone && mvn docker:build - name: Login Docker Repo run: echo "${{ secrets.ALIYUN_DOCKER_REPO_PWD }}" | docker login registry.cn-shenzhen.aliyuncs.com -u ${{ secrets.ALIYUN_DOCKER_REPO_USERNAME }} --password-stdin - name: Push Docker diff --git a/README.md b/README.md index 0f105457..0026e0b2 100644 --- a/README.md +++ b/README.md @@ -80,14 +80,14 @@ JetLinks 基于Java8,Spring Boot 2.x,WebFlux,Netty,Vert.x,Reactor等开发, | Http,WebSocket(TLS) | ⭕ | ✅ | ✅ | | 数据转发:MQTT,HTTP,Kafka... | ⭕ | ✅ | ✅ | | Geo地理位置支持 | ⭕ | ✅ | ✅ | -| 可视化图表配置 | ⭕ | ✅ | ✅ | | OpenAPI | ⭕ | ✅ | ✅ | +| 多租户(建设中) | ⭕ | ✅ | ✅ | | 集群支持 | ⭕ | ✅ | ✅ | | QQ群技术支持 | ⭕ | ✅ | ✅ | | 一对一技术支持 | ⭕ | ⭕ | ✅ | | 微服务架构(建设中) | ⭕ | ⭕ | ✅ | -| 多租户(建设中) | ⭕ | ⭕ | ✅ | | 统一认证(建设中) | ⭕ | ⭕ | ✅ | +| 选配业务模块(建设中) | ⭕ | ⭕ | ✅ | | 定制开发 | ⭕ | ⭕ | ✅ | | 商业限制 | 无 | 单个项目 | 无 | | 定价 | 免费 | 联系我们 | 联系我们 | diff --git a/docker/run-all/docker-compose-embedded.yml b/docker/run-all/docker-compose-embedded.yml index a9f98e2f..8e9bf51e 100644 --- a/docker/run-all/docker-compose-embedded.yml +++ b/docker/run-all/docker-compose-embedded.yml @@ -1,7 +1,7 @@ version: '2' services: ui: - image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-antd:1.1.1-RELEASE + image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-antd:1.2.0 container_name: jetlinks-ce-ui ports: - 9000:80 @@ -12,7 +12,7 @@ services: links: - jetlinks:jetlinks jetlinks: - image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-standalone:1.2-SNAPSHOT + image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-standalone:1.3.0-SNAPSHOT container_name: jetlinks-ce ports: - 8848:8848 # API端口 diff --git a/docker/run-all/docker-compose.yml b/docker/run-all/docker-compose.yml index 5cb60811..68ca9b55 100644 --- a/docker/run-all/docker-compose.yml +++ b/docker/run-all/docker-compose.yml @@ -48,7 +48,7 @@ services: POSTGRES_DB: jetlinks TZ: Asia/Shanghai ui: - image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-antd:1.1.1-RELEASE + image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-antd:1.2.0 container_name: jetlinks-ce-ui ports: - 9000:80 @@ -59,7 +59,7 @@ services: links: - jetlinks:jetlinks jetlinks: - image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-standalone:1.2-SNAPSHOT + image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-standalone:1.3.0-SNAPSHOT container_name: jetlinks-ce ports: - 8848:8848 # API端口 diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/Version.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/Version.java new file mode 100644 index 00000000..f7faa15c --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/Version.java @@ -0,0 +1,13 @@ +package org.jetlinks.community; + +import lombok.Getter; + +@Getter +public class Version { + public static Version current = new Version(); + + private final String edition = "community"; + + private final String version = "1.3.0-SNAPSHOT"; + +} diff --git a/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/AggregationResponseHandle.java b/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/AggregationResponseHandle.java index caacb2ef..15cb901b 100644 --- a/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/AggregationResponseHandle.java +++ b/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/AggregationResponseHandle.java @@ -9,6 +9,7 @@ import org.elasticsearch.search.aggregations.metrics.max.Max; import org.elasticsearch.search.aggregations.metrics.min.Min; import org.elasticsearch.search.aggregations.metrics.stats.Stats; import org.elasticsearch.search.aggregations.metrics.sum.Sum; +import org.elasticsearch.search.aggregations.metrics.valuecount.ValueCount; import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsResponseSingleValue; import java.util.List; @@ -97,11 +98,22 @@ public class AggregationResponseHandle { bucket.setSum(sum(a)); } else if (a instanceof Stats) { stats(bucket, a); + } else if (a instanceof ValueCount) { + bucket.setValueCount(count(a)); } else { throw new UnsupportedOperationException("不支持的聚合类型"); } } + public static MetricsResponseSingleValue count(A a) { + ValueCount max = (ValueCount) a; + return MetricsResponseSingleValue.builder() + .value(max.getValue()) + .name(a.getName()) + .valueAsString(max.getValueAsString()) + .build(); + } + public static MetricsResponseSingleValue avg(A a) { Avg avg = (Avg) a; return MetricsResponseSingleValue.builder() diff --git a/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/Bucket.java b/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/Bucket.java index 56ce1def..a79e1b12 100644 --- a/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/Bucket.java +++ b/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/Bucket.java @@ -44,31 +44,27 @@ public class Bucket { private List buckets; + private double toNumber(double number) { + return (Double.isInfinite(number) || Double.isNaN(number)) ? 0 : number; + } + public Map toMap() { Map map = new HashMap<>(); if (this.sum != null) { - map.put(sum.getName(), sum.getValue()); + map.put(sum.getName(), toNumber(sum.getValue())); } if (this.valueCount != null) { - map.put(valueCount.getName(), valueCount.getValue()); + map.put(valueCount.getName(), toNumber(valueCount.getValue())); } if (this.avg != null) { - map.put(avg.getName(), avg.getValue()); + map.put(avg.getName(), toNumber(avg.getValue())); } if (this.min != null) { - map.put(min.getName(), min.getValue()); + map.put(min.getName(), toNumber(min.getValue())); } if (this.max != null) { - map.put(max.getName(), max.getValue()); + map.put(max.getName(), toNumber(max.getValue())); } -// -// if (this.getBuckets() != null) { -// bucketFlatMap(this.getBuckets(), map); -// } return map; } - -// private void bucketFlatMap(List buckets, Map map) { -// buckets.forEach(bucket -> map.putAll(bucket.toMap())); -// } } diff --git a/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/configuration/ElasticSearchConfiguration.java b/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/configuration/ElasticSearchConfiguration.java index 61c9da95..2a0079e4 100644 --- a/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/configuration/ElasticSearchConfiguration.java +++ b/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/configuration/ElasticSearchConfiguration.java @@ -52,8 +52,8 @@ public class ElasticSearchConfiguration { return new ElasticRestClient(client, client); } - @Bean - public RestHighLevelClient elasticsearchRestHighLevelClient(ElasticRestClient client) { + @Bean(destroyMethod = "close") + public RestHighLevelClient restHighLevelClient(ElasticRestClient client) { return client.getWriteClient(); } diff --git a/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/service/DefaultElasticSearchService.java b/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/service/DefaultElasticSearchService.java index 32efd813..51a7f881 100644 --- a/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/service/DefaultElasticSearchService.java +++ b/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/service/DefaultElasticSearchService.java @@ -27,6 +27,7 @@ import org.jetlinks.community.elastic.search.index.ElasticSearchIndexManager; import org.jetlinks.community.elastic.search.utils.ElasticSearchConverter; import org.jetlinks.community.elastic.search.utils.ReactorActionListener; import org.reactivestreams.Publisher; +import org.springframework.context.annotation.DependsOn; import org.springframework.stereotype.Service; import reactor.core.publisher.BufferOverflowStrategy; import reactor.core.publisher.Flux; @@ -47,6 +48,7 @@ import java.util.stream.Collectors; **/ @Service @Slf4j +@DependsOn("restHighLevelClient") public class DefaultElasticSearchService implements ElasticSearchService { private final ElasticRestClient restClient; diff --git a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceMessageUtils.java b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceMessageUtils.java index 27870af3..34ad1683 100644 --- a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceMessageUtils.java +++ b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceMessageUtils.java @@ -1,25 +1,22 @@ package org.jetlinks.community.gateway; -import com.alibaba.fastjson.JSON; import org.jetlinks.core.message.DeviceMessage; import org.jetlinks.core.message.MessageType; -import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Optional; public class DeviceMessageUtils { + @SuppressWarnings("all") public static Optional convert(TopicMessage message){ - if (message.getMessage() instanceof EncodableMessage) { - Object nativeMessage = ((EncodableMessage) message.getMessage()).getNativePayload(); - if (nativeMessage instanceof DeviceMessage) { - return Optional.of((DeviceMessage)nativeMessage); - } else if (nativeMessage instanceof Map) { - return MessageType.convertMessage(((Map) nativeMessage)); - } + Object nativeMessage = message.convertMessage(); + if (nativeMessage instanceof DeviceMessage) { + return Optional.of((DeviceMessage)nativeMessage); + } else if (nativeMessage instanceof Map) { + return MessageType.convertMessage(((Map) nativeMessage)); } - return MessageType.convertMessage(JSON.parseObject(message.getMessage().getPayload().toString(StandardCharsets.UTF_8))); + return Optional.empty(); } } diff --git a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/TopicMessage.java b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/TopicMessage.java index fa4a2877..ef629a0b 100644 --- a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/TopicMessage.java +++ b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/TopicMessage.java @@ -1,9 +1,11 @@ package org.jetlinks.community.gateway; +import com.alibaba.fastjson.JSON; +import io.netty.buffer.ByteBufUtil; import org.jetlinks.core.message.codec.EncodedMessage; -import org.jetlinks.rule.engine.executor.PayloadType; import javax.annotation.Nonnull; +import java.nio.charset.StandardCharsets; public interface TopicMessage { @@ -31,11 +33,17 @@ public interface TopicMessage { if (getMessage() instanceof EncodableMessage) { return ((EncodableMessage) getMessage()).getNativePayload(); } - - if (getMessage().getPayloadType() == null) { - return getMessage().getBytes(); + byte[] payload = getMessage().payloadAsBytes(); + //maybe json + if (/* { }*/(payload[0] == 123 && payload[payload.length - 1] == 125) + || /* [ ] */(payload[0] == 91 && payload[payload.length - 1] == 93) + ) { + return JSON.parseObject(new String(payload)); } - return PayloadType.valueOf(getMessage().getPayloadType().name()).read(getMessage().getPayload()); + if (ByteBufUtil.isText(getMessage().getPayload(), StandardCharsets.UTF_8)) { + return getMessage().payloadAsString(); + } + return payload; } static TopicMessage of(String topic, EncodedMessage message) { diff --git a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/rule/MessageGatewayRuleNode.java b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/rule/MessageGatewayRuleNode.java index 06ed6ee3..a83df43d 100644 --- a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/rule/MessageGatewayRuleNode.java +++ b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/rule/MessageGatewayRuleNode.java @@ -1,19 +1,21 @@ package org.jetlinks.community.gateway.rule; +import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.exception.NotFoundException; import org.jetlinks.community.gateway.MessageGatewayManager; import org.jetlinks.community.network.PubSubType; import org.jetlinks.rule.engine.api.RuleData; -import org.jetlinks.rule.engine.api.executor.ExecutionContext; -import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy; +import org.jetlinks.rule.engine.api.task.ExecutionContext; +import org.jetlinks.rule.engine.api.task.TaskExecutor; +import org.jetlinks.rule.engine.api.task.TaskExecutorProvider; +import org.jetlinks.rule.engine.defaults.FunctionTaskExecutor; import org.reactivestreams.Publisher; import org.springframework.stereotype.Component; +import reactor.core.Disposable; import reactor.core.publisher.Mono; -import java.util.function.Function; - @Component -public class MessageGatewayRuleNode extends CommonExecutableRuleNodeFactoryStrategy { +public class MessageGatewayRuleNode implements TaskExecutorProvider { private final MessageGatewayManager gatewayManager; @@ -26,43 +28,52 @@ public class MessageGatewayRuleNode extends CommonExecutableRuleNodeFactoryStrat } @Override - public Function> createExecutor(ExecutionContext context, MessageGatewayRuleNodeConfig config) { - if (config.getType() == PubSubType.consumer) { - return Mono::just; - } - return ruleData -> gatewayManager - .getGateway(config.getGatewayId()) - .switchIfEmpty(Mono.error(() -> new NotFoundException("消息网关[{" + config.getGatewayId() + "}]不存在"))) - .flatMap(gateway -> config.convert(ruleData) - .flatMap(msg -> gateway.publish(msg, config.isShareCluster())) - .then()) - .thenReturn(ruleData); - } - - @Override - protected void onStarted(ExecutionContext context, MessageGatewayRuleNodeConfig config) { - super.onStarted(context, config); - if (config.getType() == PubSubType.producer) { - return; - } - //订阅网关中的消息 - context.onStop(gatewayManager - .getGateway(config.getGatewayId()) - .switchIfEmpty(Mono.fromRunnable(() -> context.logger().error("消息网关[{" + config.getGatewayId() + "}]不存在"))) - .flatMapMany(gateway -> gateway.subscribe(config.createTopics())) - .map(config::convert) - .flatMap(data -> context.getOutput().write(Mono.just(RuleData.create(data)))) - .onErrorContinue((err, obj) -> { - context.logger().error(err.getMessage(), err); - }) - .subscribe()::dispose); - - } - - @Override - public String getSupportType() { + public String getExecutor() { return "message-gateway"; } + @Override + public Mono createTask(ExecutionContext context) { + return Mono.just(new MessageGatewayPubSubExecutor(context)); + } + class MessageGatewayPubSubExecutor extends FunctionTaskExecutor { + MessageGatewayRuleNodeConfig config; + + public MessageGatewayPubSubExecutor(ExecutionContext context) { + super("消息网关订阅发布", context); + this.config = FastBeanCopier.copy(context.getJob().getConfiguration(), MessageGatewayRuleNodeConfig.class); + this.config.validate(); + } + + @Override + protected Publisher apply(RuleData input) { + return gatewayManager + .getGateway(config.getGatewayId()) + .switchIfEmpty(Mono.error(() -> new NotFoundException("消息网关[{" + config.getGatewayId() + "}]不存在"))) + .flatMap(gateway -> config.convert(input) + .flatMap(msg -> gateway.publish(msg, config.isShareCluster())) + .then()) + .thenReturn(input); + } + + @Override + protected Disposable doStart() { + if (config.getType() == PubSubType.producer) { + return super.doStart(); + } + + //订阅网关中的消息 + return gatewayManager + .getGateway(config.getGatewayId()) + .switchIfEmpty(Mono.fromRunnable(() -> context.getLogger().error("消息网关[{" + config.getGatewayId() + "}]不存在"))) + .flatMapMany(gateway -> gateway.subscribe(config.createTopics())) + .map(config::convert) + .flatMap(data -> context.getOutput().write(Mono.just(RuleData.create(data)))) + .onErrorContinue((err, obj) -> { + context.getLogger().error(err.getMessage(), err); + }) + .subscribe(); + } + } } diff --git a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/rule/MessageGatewayRuleNodeConfig.java b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/rule/MessageGatewayRuleNodeConfig.java index 8458273c..36c1528d 100644 --- a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/rule/MessageGatewayRuleNodeConfig.java +++ b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/rule/MessageGatewayRuleNodeConfig.java @@ -5,14 +5,12 @@ import lombok.Setter; import org.jetlinks.community.gateway.TopicMessage; import org.jetlinks.community.network.PubSubType; import org.jetlinks.rule.engine.api.RuleData; -import org.jetlinks.rule.engine.api.model.NodeType; -import org.jetlinks.rule.engine.executor.node.RuleNodeConfig; import org.springframework.util.Assert; import reactor.core.publisher.Flux; @Getter @Setter -public class MessageGatewayRuleNodeConfig implements RuleNodeConfig { +public class MessageGatewayRuleNodeConfig { private String gatewayId; @@ -35,7 +33,6 @@ public class MessageGatewayRuleNodeConfig implements RuleNodeConfig { return topics.split("[,;\n]"); } - @Override public void validate() { Assert.hasText(gatewayId, "gatewayId can not be empty"); Assert.hasText(topics, "topics can not be empty"); @@ -43,14 +40,4 @@ public class MessageGatewayRuleNodeConfig implements RuleNodeConfig { } - @Override - public NodeType getNodeType() { - return NodeType.MAP; - } - - - @Override - public void setNodeType(NodeType nodeType) { - - } } \ No newline at end of file diff --git a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/spring/ProxyMessageListener.java b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/spring/ProxyMessageListener.java index ec4bd510..9beaa852 100644 --- a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/spring/ProxyMessageListener.java +++ b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/spring/ProxyMessageListener.java @@ -15,8 +15,8 @@ import java.util.StringJoiner; import java.util.function.BiFunction; class ProxyMessageListener implements MessageListener { - private Class paramType; - private Object target; + private final Class paramType; + private final Object target; BiFunction proxy; @@ -72,11 +72,14 @@ class ProxyMessageListener implements MessageListener { return message.getMessage().getPayload(); } - if (message.getMessage() instanceof EncodableMessage) { - Object payload = ((EncodableMessage) message.getMessage()).getNativePayload(); - return FastBeanCopier.DEFAULT_CONVERT.convert(payload, paramType, new Class[]{}); + Object payload = message.convertMessage(); + if (paramType.isInstance(payload)) { + return payload; } - return message; + if (payload instanceof byte[]) { + return payload; + } + return FastBeanCopier.DEFAULT_CONVERT.convert(payload, paramType, new Class[]{}); } diff --git a/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttClientTaskConfiguration.java b/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttClientTaskConfiguration.java new file mode 100644 index 00000000..c674db41 --- /dev/null +++ b/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttClientTaskConfiguration.java @@ -0,0 +1,39 @@ +package org.jetlinks.community.network.mqtt.executor; + +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.utils.ExpressionUtils; +import org.jetlinks.community.network.PubSubType; +import org.jetlinks.rule.engine.executor.PayloadType; +import org.springframework.util.Assert; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Getter +@Setter +public class MqttClientTaskConfiguration { + + private String clientId; + + private PayloadType payloadType = PayloadType.JSON; + + private PubSubType[] clientType; + + private List topics; + + private List topicVariables; + + public List getTopics(Map vars) { + return topics.stream() + .map(topic -> ExpressionUtils.analytical(topic, vars, "spel")).collect(Collectors.toList()); + } + + public void validate() { + Assert.hasText(clientId, "clientId can not be empty"); + Assert.notNull(clientType, "clientType can not be null"); + Assert.notEmpty(topics, "topics can not be empty"); + + } +} diff --git a/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttClientTaskExecutorProvider.java b/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttClientTaskExecutorProvider.java new file mode 100644 index 00000000..e6598dff --- /dev/null +++ b/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttClientTaskExecutorProvider.java @@ -0,0 +1,131 @@ +package org.jetlinks.community.network.mqtt.executor; + +import lombok.AllArgsConstructor; +import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.dict.EnumDict; +import org.jetlinks.core.message.codec.MqttMessage; +import org.jetlinks.community.network.DefaultNetworkType; +import org.jetlinks.community.network.NetworkManager; +import org.jetlinks.community.network.PubSubType; +import org.jetlinks.community.network.mqtt.client.MqttClient; +import org.jetlinks.rule.engine.api.RuleConstants; +import org.jetlinks.rule.engine.api.RuleData; +import org.jetlinks.rule.engine.api.RuleDataCodecs; +import org.jetlinks.rule.engine.api.RuleDataHelper; +import org.jetlinks.rule.engine.api.task.ExecutionContext; +import org.jetlinks.rule.engine.api.task.Task; +import org.jetlinks.rule.engine.api.task.TaskExecutor; +import org.jetlinks.rule.engine.api.task.TaskExecutorProvider; +import org.jetlinks.rule.engine.defaults.AbstractTaskExecutor; +import org.springframework.stereotype.Component; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@AllArgsConstructor +@Component +public class MqttClientTaskExecutorProvider implements TaskExecutorProvider { + + private final NetworkManager networkManager; + + static { + MqttRuleDataCodec.load(); + } + + @Override + public String getExecutor() { + return "mqtt-client"; + } + + protected Flux convertMessage(RuleData message, MqttClientTaskConfiguration config) { + + return RuleDataCodecs.getCodec(MqttMessage.class) + .map(codec -> + codec.decode(message, + config.getPayloadType(), + new MqttTopics(config.getTopics(RuleDataHelper.toContextMap(message)))) + .cast(MqttMessage.class)) + .orElseThrow(() -> new UnsupportedOperationException("unsupported decode message:{}" + message)); + } + + protected Mono convertMessage(MqttMessage message, MqttClientTaskConfiguration config) { + + return Mono.just(RuleDataCodecs.getCodec(MqttMessage.class) + .map(codec -> codec.encode(message, config.getPayloadType(), new TopicVariables(config.getTopicVariables()))) + .map(RuleData::create) + .orElseGet(() -> RuleData.create(message))); + } + + @Override + public Mono createTask(ExecutionContext context) { + return Mono.just(new MqttClientTaskExecutor(context)); + } + + class MqttClientTaskExecutor extends AbstractTaskExecutor { + + private MqttClientTaskConfiguration config; + + public MqttClientTaskExecutor(ExecutionContext context) { + super(context); + reload(); + } + + @Override + public String getName() { + return "MQTT Client"; + } + + @Override + public void reload() { + config = FastBeanCopier.copy(context.getJob().getConfiguration(), new MqttClientTaskConfiguration()); + config.validate(); + if (disposable != null) { + disposable.dispose(); + } + } + + @Override + public void validate() { + FastBeanCopier + .copy(context.getJob().getConfiguration(), new MqttClientTaskConfiguration()) + .validate(); + } + + @Override + protected Disposable doStart() { + Disposable.Composite disposable = Disposables.composite(); + + if (EnumDict.in(PubSubType.producer, config.getClientType())) { + disposable.add(context.getInput() + .accept() + .filter((data) -> state == Task.State.running) + .flatMap(data -> + networkManager + .getNetwork(DefaultNetworkType.MQTT_CLIENT, config.getClientId()) + .flatMapMany(client -> convertMessage(data, config) + .flatMap(msg -> client + .publish(msg) + .doOnSuccess((v) -> context.getLogger().debug("推送MQTT[{}]消息:{}", client.getId(), msg)) + ) + ).onErrorContinue((err, e) -> context.onError(err, null).subscribe()) + ) + .subscribe() + ); + } + if (EnumDict.in(PubSubType.consumer, config.getClientType())) { + disposable.add(networkManager + .getNetwork(DefaultNetworkType.MQTT_CLIENT, config.getClientId()) + .flatMapMany(client -> client.subscribe(config.getTopics())) + .filter((data) -> state == Task.State.running) + .doOnNext(message -> context.getLogger().info("consume mqtt message:{}", message)) + .flatMap(message -> convertMessage(message, config)) + .flatMap(ruleData -> context.getOutput().write(Mono.just(ruleData)).thenReturn(ruleData)) + .flatMap(ruleData -> context.fireEvent(RuleConstants.Event.result, ruleData).thenReturn(ruleData)) + .onErrorContinue((err, e) -> context.onError(err, null).subscribe()) + .subscribe()); + } + return disposable; + } + } +} diff --git a/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttRuleDataCodec.java b/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttRuleDataCodec.java new file mode 100644 index 00000000..bd63f373 --- /dev/null +++ b/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttRuleDataCodec.java @@ -0,0 +1,111 @@ +package org.jetlinks.community.network.mqtt.executor; + +import io.netty.buffer.ByteBuf; +import org.apache.commons.collections4.CollectionUtils; +import org.jetlinks.core.message.codec.MessagePayloadType; +import org.jetlinks.core.message.codec.MqttMessage; +import org.jetlinks.core.message.codec.SimpleMqttMessage; +import org.jetlinks.rule.engine.api.RuleData; +import org.jetlinks.rule.engine.api.RuleDataCodec; +import org.jetlinks.rule.engine.api.RuleDataCodecs; +import org.jetlinks.rule.engine.executor.PayloadType; +import org.jetlinks.supports.utils.MqttTopicUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class MqttRuleDataCodec implements RuleDataCodec { + + static { + + MqttRuleDataCodec codec = new MqttRuleDataCodec(); +// EncodedMessageCodec.register(DefaultTransport.MQTT, codec); +// EncodedMessageCodec.register(DefaultTransport.MQTT_TLS, codec); + RuleDataCodecs.register(MqttMessage.class, codec); + + } + + static void load() { + + } + + @Override + public Object encode(MqttMessage message, Feature... features) { + Map payload = new HashMap<>(); + payload.put("topic", message.getTopic()); + payload.put("will", message.isWill()); + payload.put("qos", message.getQosLevel()); + payload.put("dup", message.isDup()); + payload.put("retain", message.isRetain()); + PayloadType payloadType = Feature.find(PayloadType.class, features).orElse(PayloadType.JSON); + Feature.find(TopicVariables.class, features) + .map(TopicVariables::getVariables) + .filter(CollectionUtils::isNotEmpty) + .flatMap(list -> list.stream() + .map(str -> MqttTopicUtils.getPathVariables(str, message.getTopic())) + .reduce((m1, m2) -> { + m1.putAll(m2); + return m1; + })) + .ifPresent(vars -> payload.put("vars", vars)); + + payload.put("payloadType", payloadType.name()); + payload.put("payload", payloadType.read(message.getPayload())); + payload.put("clientId", message.getClientId()); + + + return payload; + } + + @Override + public Flux decode(RuleData data, Feature... features) { + if (data.getData() instanceof MqttMessage) { + return Flux.just(((MqttMessage) data.getData())); + } + MqttTopics topics = Feature.find(MqttTopics.class, features).orElse(null); + + return data + .dataToMap() + .filter(map -> map.containsKey("payload")) + .flatMap(map -> { + if (topics != null && !map.containsKey("topic")) { + return Flux.fromIterable(topics.getTopics()) + .flatMap(topic -> { + Map copy = new HashMap<>(); + copy.put("topic", topic); + copy.putAll(map); + return Mono.just(copy); + }) + .switchIfEmpty(Mono.error(() -> new UnsupportedOperationException("topic not set"))); + } + return Flux.just(map); + }) + .map(map -> { + PayloadType payloadType = Feature.find(PayloadType.class, features) + .orElseGet(() -> Optional.ofNullable(map.get("payloadType")) + .map(String::valueOf) + .map(PayloadType::valueOf) + .orElse(PayloadType.JSON)); + Object payload = map.get("payload"); + + ByteBuf byteBuf = payloadType.write(payload); + + Integer qos = (Integer) map.get("qos"); + + return SimpleMqttMessage + .builder() + .clientId((String) map.get("clientId")) + .topic((String) map.get("topic")) + .dup(Boolean.TRUE.equals(map.get("dup"))) + .will(Boolean.TRUE.equals(map.get("will"))) + .retain(Boolean.TRUE.equals(map.get("retain"))) + .qosLevel(qos == null ? 0 : qos) + .payloadType(MessagePayloadType.valueOf(payloadType.name())) + .payload(byteBuf) + .build(); + }); + } +} \ No newline at end of file diff --git a/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttTopics.java b/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttTopics.java new file mode 100644 index 00000000..d7d43ccd --- /dev/null +++ b/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttTopics.java @@ -0,0 +1,24 @@ +package org.jetlinks.community.network.mqtt.executor; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.jetlinks.rule.engine.api.RuleDataCodec; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class MqttTopics implements RuleDataCodec.Feature { + + private List topics; + + @Override + public String getId() { + return "mqtt-topic"; + } + + @Override + public String getName() { + return "MQTT Topics"; + } +} diff --git a/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/TopicVariables.java b/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/TopicVariables.java new file mode 100644 index 00000000..5ecad7ef --- /dev/null +++ b/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/TopicVariables.java @@ -0,0 +1,13 @@ +package org.jetlinks.community.network.mqtt.executor; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.jetlinks.rule.engine.api.RuleDataCodec; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class TopicVariables implements RuleDataCodec.Feature { + List variables; +} diff --git a/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/node/MqttClientNode.java b/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/node/MqttClientNode.java deleted file mode 100644 index f1355544..00000000 --- a/jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/node/MqttClientNode.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.jetlinks.community.network.mqtt.node; - -import lombok.AllArgsConstructor; -import org.hswebframework.web.dict.EnumDict; -import org.jetlinks.community.network.mqtt.client.MqttClient; -import org.jetlinks.core.message.codec.MqttMessage; -import org.jetlinks.community.network.DefaultNetworkType; -import org.jetlinks.community.network.NetworkManager; -import org.jetlinks.rule.engine.api.RuleData; -import org.jetlinks.rule.engine.api.RuleDataCodecs; -import org.jetlinks.rule.engine.api.RuleDataHelper; -import org.jetlinks.rule.engine.api.events.RuleEvent; -import org.jetlinks.rule.engine.api.executor.ExecutionContext; -import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy; -import org.jetlinks.rule.engine.executor.node.mqtt.*; -import org.reactivestreams.Publisher; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.util.function.Function; - -@AllArgsConstructor -@Component -public class MqttClientNode extends CommonExecutableRuleNodeFactoryStrategy { - - private NetworkManager networkManager; - - static { - try { - Class.forName("org.jetlinks.rule.engine.executor.node.mqtt.MqttRuleDataCodec"); - } catch (ClassNotFoundException ignore) { - - } - } - - @Override - public Function> createExecutor(ExecutionContext context, MqttClientConfiguration config) { - - if (!EnumDict.in(ClientType.producer, config.getClientType())) { - return Mono::just; - } - return ruleData -> networkManager - .getNetwork(DefaultNetworkType.MQTT_CLIENT, config.getClientId()) - .flatMapMany(client -> this.convertMessage(ruleData, config).flatMap(client::publish)) - .then(Mono.just(ruleData)) - ; - } - - protected Flux convertMessage(RuleData message, MqttClientConfiguration config) { - - return RuleDataCodecs.getCodec(MqttMessage.class) - .map(codec -> - codec.decode(message, - config.getPayloadType(), - new MqttTopics(config.getTopics(RuleDataHelper.toContextMap(message)))) - .cast(MqttMessage.class)) - .orElseThrow(() -> new UnsupportedOperationException("unsupported decode message:{}" + message)); - } - - protected Mono convertMessage(MqttMessage message, MqttClientConfiguration config) { - - return Mono.just(RuleDataCodecs.getCodec(MqttMessage.class) - .map(codec -> codec.encode(message, config.getPayloadType(), new TopicVariables(config.getTopicVariables()))) - .map(RuleData::create) - .orElseGet(() -> RuleData.create(message))); - } - - - @Override - protected void onStarted(ExecutionContext context, MqttClientConfiguration config) { - if (!EnumDict.in(ClientType.consumer, config.getClientType())) { - return; - } - context.onStop(networkManager - .getNetwork(DefaultNetworkType.MQTT_CLIENT, config.getClientId()) - .flatMapMany(client -> client.subscribe(config.getTopics())) - .doOnNext(message -> context.logger().info("consume mqtt message:{}", message)) - .flatMap(message -> convertMessage(message, config)) - .flatMap(ruleData -> context.getOutput().write(Mono.just(ruleData)).thenReturn(ruleData)) - .flatMap(ruleData -> context.fireEvent(RuleEvent.NODE_EXECUTE_RESULT, ruleData).thenReturn(ruleData)) - .onErrorContinue((err, e) -> context.onError(RuleData.create("consume mqtt message error"), err).subscribe()) - .subscribe()::dispose); - } - - @Override - public String getSupportType() { - return "mqtt-client"; - } -} diff --git a/jetlinks-components/network-component/network-core/src/main/java/org/jetlinks/community/network/PubSubType.java b/jetlinks-components/network-component/network-core/src/main/java/org/jetlinks/community/network/PubSubType.java index 1bcdfea7..e77c4853 100644 --- a/jetlinks-components/network-component/network-core/src/main/java/org/jetlinks/community/network/PubSubType.java +++ b/jetlinks-components/network-component/network-core/src/main/java/org/jetlinks/community/network/PubSubType.java @@ -1,7 +1,21 @@ package org.jetlinks.community.network; -public enum PubSubType { +import lombok.Getter; +import org.hswebframework.web.dict.EnumDict; + +@Getter +public enum PubSubType implements EnumDict { producer, consumer; + + @Override + public String getValue() { + return name(); + } + + @Override + public String getText() { + return name(); + } } diff --git a/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/client/VertxTcpClient.java b/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/client/VertxTcpClient.java index 436bf5d8..798c52b5 100644 --- a/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/client/VertxTcpClient.java +++ b/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/client/VertxTcpClient.java @@ -30,14 +30,14 @@ public class VertxTcpClient extends AbstractTcpClient { volatile PayloadParser payloadParser; @Getter - private String id; + private final String id; @Setter private long keepAliveTimeoutMs = Duration.ofMinutes(10).toMillis(); private volatile long lastKeepAliveTime = System.currentTimeMillis(); - private List disconnectListener = new CopyOnWriteArrayList<>(); + private final List disconnectListener = new CopyOnWriteArrayList<>(); @Override public void keepAlive() { diff --git a/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/device/TcpServerDeviceGateway.java b/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/device/TcpServerDeviceGateway.java index c323b130..89f34221 100644 --- a/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/device/TcpServerDeviceGateway.java +++ b/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/device/TcpServerDeviceGateway.java @@ -44,23 +44,31 @@ import java.util.function.Function; class TcpServerDeviceGateway implements DeviceGateway, MonitorSupportDeviceGateway { @Getter - private String id; + private final String id; - private TcpServer tcpServer; + private final TcpServer tcpServer; - private String protocol; + private final String protocol; - private ProtocolSupports supports; + private final ProtocolSupports supports; - private DeviceRegistry registry; + private final DeviceRegistry registry; - private DecodedClientMessageHandler clientMessageHandler; + private final DecodedClientMessageHandler clientMessageHandler; - private DeviceSessionManager sessionManager; + private final DeviceSessionManager sessionManager; - private DeviceGatewayMonitor gatewayMonitor; + private final DeviceGatewayMonitor gatewayMonitor; - private LongAdder counter = new LongAdder(); + private final LongAdder counter = new LongAdder(); + + private final EmitterProcessor processor = EmitterProcessor.create(false); + + private final FluxSink sink = processor.sink(); + + private final AtomicBoolean started = new AtomicBoolean(); + + private final List disposable = new CopyOnWriteArrayList<>(); public TcpServerDeviceGateway(String id, String protocol, @@ -80,12 +88,6 @@ class TcpServerDeviceGateway implements DeviceGateway, MonitorSupportDeviceGatew } - private EmitterProcessor processor = EmitterProcessor.create(false); - - private FluxSink sink = processor.sink(); - - private AtomicBoolean started = new AtomicBoolean(); - public Mono getProtocol() { return supports.getProtocol(protocol); } @@ -105,8 +107,6 @@ class TcpServerDeviceGateway implements DeviceGateway, MonitorSupportDeviceGatew return DefaultNetworkType.TCP_SERVER; } - private List disposable = new CopyOnWriteArrayList<>(); - private void doStart() { if (started.getAndSet(true) || !disposable.isEmpty()) { return; @@ -127,7 +127,7 @@ class TcpServerDeviceGateway implements DeviceGateway, MonitorSupportDeviceGatew AtomicReference sessionRef = new AtomicReference<>(sessionManager.getSession(client.getId())); client.subscribe() .filter(r -> started.get()) - .takeWhile(r -> disposable != null) + .takeWhile(r -> !disposable.isEmpty()) .doOnNext(r -> { log.debug("收到TCP报文:\n{}", r); gatewayMonitor.receivedMessage(); @@ -165,11 +165,6 @@ class TcpServerDeviceGateway implements DeviceGateway, MonitorSupportDeviceGatew return getSession().getOperator(); } })) - .switchIfEmpty(Mono.fromRunnable(() -> - log.warn("无法识别的TCP客户端[{}]消息:\n{}", - clientAddr, - tcpMessage - ))) .cast(DeviceMessage.class) .flatMap(message -> registry .getDevice(message.getDeviceId()) @@ -182,20 +177,20 @@ class TcpServerDeviceGateway implements DeviceGateway, MonitorSupportDeviceGatew ); })) .flatMap(device -> { - DeviceSession fSession = sessionRef.get() == null ? - sessionManager.getSession(device.getDeviceId()) : - sessionRef.get(); + DeviceSession fSession = sessionManager.getSession(device.getDeviceId()); //处理设备上线消息 if (message instanceof DeviceOnlineMessage) { if (fSession == null) { - fSession = new TcpDeviceSession(client.getId(), device, client, getTransport()) { + boolean keepOnline = message.getHeader(Headers.keepOnline).orElse(false); + String sessionId = device.getDeviceId(); + fSession = new TcpDeviceSession(sessionId, device, client, getTransport()) { @Override public Mono send(EncodedMessage encodedMessage) { return super.send(encodedMessage).doOnSuccess(r -> gatewayMonitor.sentMessage()); } }; - //保持设备一直在线.(通过短连接上报数据的场景.可以让设备一直为在线状态) - if (message.getHeader(Headers.keepOnline).orElse(false)) { + //保持设备一直在线.(短连接上报数据的场景.可以让设备一直为在线状态) + if (keepOnline) { fSession = new KeepOnlineSession(fSession, Duration.ofMillis(-1)); } else { client.onDisconnect(() -> sessionManager.unregister(device.getDeviceId())); diff --git a/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/executor/TcpClientTaskConfiguration.java b/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/executor/TcpClientTaskConfiguration.java new file mode 100644 index 00000000..237761d0 --- /dev/null +++ b/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/executor/TcpClientTaskConfiguration.java @@ -0,0 +1,25 @@ +package org.jetlinks.community.network.tcp.executor; + +import lombok.Getter; +import lombok.Setter; +import org.jetlinks.community.network.PubSubType; +import org.jetlinks.rule.engine.executor.PayloadType; +import org.springframework.util.Assert; + +@Getter +@Setter +public class TcpClientTaskConfiguration { + + private String clientId; + + private PubSubType type; + + private PayloadType payloadType; + + public void validate() { + Assert.hasText(clientId, "clientId can not be empty!"); + Assert.notNull(type, "type can not be null!"); + Assert.notNull(payloadType, "type can not be null!"); + + } +} diff --git a/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/executor/TcpClientTaskExecutorProvider.java b/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/executor/TcpClientTaskExecutorProvider.java new file mode 100644 index 00000000..218460b6 --- /dev/null +++ b/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/executor/TcpClientTaskExecutorProvider.java @@ -0,0 +1,109 @@ +package org.jetlinks.community.network.tcp.executor; + +import lombok.AllArgsConstructor; +import org.hswebframework.web.bean.FastBeanCopier; +import org.jetlinks.community.network.DefaultNetworkType; +import org.jetlinks.community.network.NetworkManager; +import org.jetlinks.community.network.PubSubType; +import org.jetlinks.community.network.tcp.TcpMessage; +import org.jetlinks.community.network.tcp.client.TcpClient; +import org.jetlinks.rule.engine.api.RuleData; +import org.jetlinks.rule.engine.api.RuleDataCodecs; +import org.jetlinks.rule.engine.api.task.ExecutionContext; +import org.jetlinks.rule.engine.api.task.TaskExecutor; +import org.jetlinks.rule.engine.api.task.TaskExecutorProvider; +import org.jetlinks.rule.engine.defaults.AbstractTaskExecutor; +import org.springframework.stereotype.Component; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@AllArgsConstructor +@Component +public class TcpClientTaskExecutorProvider implements TaskExecutorProvider { + + private final NetworkManager clientManager; + + static { + TcpMessageCodec.register(); + } + + @Override + public String getExecutor() { + return "tcp-client"; + } + + @Override + public Mono createTask(ExecutionContext context) { + return Mono.just(new TcpTaskExecutor(context)); + } + + class TcpTaskExecutor extends AbstractTaskExecutor { + + private TcpClientTaskConfiguration config; + + public TcpTaskExecutor(ExecutionContext context) { + super(context); + reload(); + } + + @Override + public String getName() { + return "Tcp Client"; + } + + @Override + public void reload() { + config = FastBeanCopier.copy(context.getJob().getConfiguration(), new TcpClientTaskConfiguration()); + config.validate(); + } + + @Override + public void validate() { + FastBeanCopier + .copy(context.getJob().getConfiguration(), new TcpClientTaskConfiguration()) + .validate(); + } + + @Override + protected Disposable doStart() { + Disposable.Composite disposable = Disposables.composite(); + + if (config.getType() == PubSubType.producer) { + disposable.add(context + .getInput() + .accept() + .flatMap(data -> + clientManager.getNetwork(DefaultNetworkType.TCP_CLIENT, config.getClientId()) + .flatMapMany(client -> RuleDataCodecs + .getCodec(TcpMessage.class) + .map(codec -> codec.decode(data, config.getPayloadType()) + .cast(TcpMessage.class) + .switchIfEmpty(Mono.fromRunnable(() -> context.getLogger().warn("can not decode rule data to tcp message:{}", data)))) + .orElseGet(() -> Flux.just(new TcpMessage(config.getPayloadType().write(data.getData())))) + .flatMap(client::send) + .onErrorContinue((err, r) -> { + context.onError(err, data).subscribe(); + }) + .then() + )).subscribe() + ) + ; + } + if (config.getType() == PubSubType.consumer) { + disposable.add(clientManager.getNetwork(DefaultNetworkType.TCP_CLIENT, config.getClientId()) + .switchIfEmpty(Mono.fromRunnable(() -> context.getLogger().error("tcp client {} not found", config.getClientId()))) + .flatMapMany(TcpClient::subscribe) + .doOnNext(msg -> context.getLogger().info("received tcp client message:{}", config.getPayloadType().read(msg.getPayload()))) + .map(r -> RuleDataCodecs.getCodec(TcpMessage.class) + .map(codec -> codec.encode(r, config.getPayloadType())) + .orElse(r.getPayload())) + .flatMap(out -> context.getOutput().write(Mono.just(RuleData.create(out)))) + .onErrorContinue((err, obj) -> context.getLogger().error("consume tcp message error", err)) + .subscribe()); + } + return disposable; + } + } +} diff --git a/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/node/TcpMessageCodec.java b/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/executor/TcpMessageCodec.java similarity index 85% rename from jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/node/TcpMessageCodec.java rename to jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/executor/TcpMessageCodec.java index 25b29d0d..8eadcf30 100644 --- a/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/node/TcpMessageCodec.java +++ b/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/executor/TcpMessageCodec.java @@ -1,12 +1,10 @@ -package org.jetlinks.community.network.tcp.node; +package org.jetlinks.community.network.tcp.executor; -import org.jetlinks.core.message.codec.DefaultTransport; import org.jetlinks.community.network.tcp.TcpMessage; import org.jetlinks.rule.engine.api.RuleData; import org.jetlinks.rule.engine.api.RuleDataCodec; import org.jetlinks.rule.engine.api.RuleDataCodecs; import org.jetlinks.rule.engine.executor.PayloadType; -import org.jetlinks.rule.engine.executor.node.device.EncodedMessageCodec; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -19,9 +17,6 @@ public class TcpMessageCodec implements RuleDataCodec { static { RuleDataCodecs.register(TcpMessage.class, instance); - - EncodedMessageCodec.register(DefaultTransport.TCP, instance); - EncodedMessageCodec.register(DefaultTransport.TCP_TLS, instance); } static void register() { diff --git a/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/node/TcpClientNode.java b/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/node/TcpClientNode.java deleted file mode 100644 index 4dc89a21..00000000 --- a/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/node/TcpClientNode.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.jetlinks.community.network.tcp.node; - -import lombok.AllArgsConstructor; -import org.jetlinks.community.network.DefaultNetworkType; -import org.jetlinks.community.network.NetworkManager; -import org.jetlinks.community.network.PubSubType; -import org.jetlinks.community.network.tcp.TcpMessage; -import org.jetlinks.community.network.tcp.client.TcpClient; -import org.jetlinks.rule.engine.api.RuleData; -import org.jetlinks.rule.engine.api.RuleDataCodecs; -import org.jetlinks.rule.engine.api.executor.ExecutionContext; -import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy; -import org.reactivestreams.Publisher; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.util.function.Function; - -@AllArgsConstructor -@Component -public class TcpClientNode extends CommonExecutableRuleNodeFactoryStrategy { - - private NetworkManager clientManager; - - static { - TcpMessageCodec.register(); - } - - @Override - public Function> createExecutor(ExecutionContext context, TcpClientNodeConfig config) { - - if (config.getType() != PubSubType.producer) { - return Mono::just; - } - return data -> clientManager.getNetwork(DefaultNetworkType.TCP_CLIENT,config.getClientId()) - .flatMapMany(client -> RuleDataCodecs - .getCodec(TcpMessage.class) - .map(codec -> codec.decode(data, config.getPayloadType()) - .cast(TcpMessage.class) - .switchIfEmpty(Mono.fromRunnable(() -> context.logger().warn("can not decode rule data to tcp message:{}", data)))) - .orElseGet(() -> Flux.just(new TcpMessage(config.getPayloadType().write(data.getData())))) - .flatMap(client::send) - .all(r-> r)) - ; - } - - @Override - protected void onStarted(ExecutionContext context, TcpClientNodeConfig config) { - super.onStarted(context, config); - if (config.getType() == PubSubType.consumer) { - context.onStop( clientManager.getNetwork(DefaultNetworkType.TCP_CLIENT,config.getClientId()) - .switchIfEmpty(Mono.fromRunnable(() -> context.logger().error("tcp client {} not found", config.getClientId()))) - .flatMapMany(TcpClient::subscribe) - .doOnNext(msg -> context.logger().info("received tcp client message:{}", config.getPayloadType().read(msg.getPayload()))) - .map(r -> RuleDataCodecs.getCodec(TcpMessage.class) - .map(codec -> codec.encode(r, config.getPayloadType())) - .orElse(r.getPayload())) - .onErrorContinue((err, obj) -> { - context.logger().error("consume tcp message error", err); - }) - .subscribe(msg -> context.getOutput().write(Mono.just(RuleData.create(msg))).subscribe())::dispose); - } - } - - @Override - public String getSupportType() { - return "tcp-client"; - } -} diff --git a/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/node/TcpClientNodeConfig.java b/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/node/TcpClientNodeConfig.java deleted file mode 100644 index ce9a3668..00000000 --- a/jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/node/TcpClientNodeConfig.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.jetlinks.community.network.tcp.node; - -import lombok.Getter; -import lombok.Setter; -import org.jetlinks.community.network.PubSubType; -import org.jetlinks.rule.engine.api.model.NodeType; -import org.jetlinks.rule.engine.executor.PayloadType; -import org.jetlinks.rule.engine.executor.node.RuleNodeConfig; -import org.springframework.util.Assert; - -@Getter -@Setter -public class TcpClientNodeConfig implements RuleNodeConfig { - - private String clientId; - - private PubSubType type; - - private PayloadType payloadType; - - @Override - public NodeType getNodeType() { - return NodeType.MAP; - } - - @Override - public void setNodeType(NodeType nodeType) { - - } - - @Override - public void validate() { - Assert.hasText(clientId, "clientId can not be empty!"); - Assert.notNull(type, "type can not be null!"); - Assert.notNull(payloadType, "payloadType can not be null!"); - - } -} diff --git a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/event/SerializableNotifierEvent.java b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/event/SerializableNotifierEvent.java index 1df7df8a..028bce63 100644 --- a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/event/SerializableNotifierEvent.java +++ b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/event/SerializableNotifierEvent.java @@ -1,8 +1,6 @@ package org.jetlinks.community.notify.event; -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; +import lombok.*; import org.jetlinks.community.notify.template.Template; import javax.annotation.Nonnull; @@ -12,6 +10,8 @@ import java.util.Map; @Getter @Setter @Builder +@NoArgsConstructor +@AllArgsConstructor public class SerializableNotifierEvent { private boolean success; diff --git a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/rule/NotifierRuleNode.java b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/rule/NotifierRuleNode.java deleted file mode 100644 index 0bd6fe43..00000000 --- a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/rule/NotifierRuleNode.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.jetlinks.community.notify.rule; - -import lombok.AllArgsConstructor; -import org.jetlinks.core.Values; -import org.jetlinks.community.notify.NotifierManager; -import org.jetlinks.rule.engine.api.RuleData; -import org.jetlinks.rule.engine.api.RuleDataHelper; -import org.jetlinks.rule.engine.api.executor.ExecutionContext; -import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy; -import org.reactivestreams.Publisher; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Mono; - -import java.util.function.Function; - -@Component -@AllArgsConstructor -public class NotifierRuleNode extends CommonExecutableRuleNodeFactoryStrategy { - - private NotifierManager notifierManager; - - @Override - public String getSupportType() { - return "notifier"; - } - - @Override - public Function> createExecutor(ExecutionContext context, RuleNotifierProperties config) { - return rule -> notifierManager - .getNotifier(config.getNotifyType(), config.getNotifierId()) - .switchIfEmpty(Mono.fromRunnable(() -> { - context.logger().warn("通知器[{}-{}]不存在", config.getNodeType(), config.getNotifierId()); - })) - .flatMap(notifier -> notifier.send(config.getTemplateId(), Values.of(RuleDataHelper.toContextMap(rule)))) - .doOnError(err -> { - context.logger().error("发送[{}]通知[{}-{}]失败", - config.getNotifyType().getName(), - config.getNotifierId(), - config.getTemplateId(), err); - }) - .doOnSuccess(ignore -> { - context.logger().info("发送[{}]通知[{}-{}]完成", - config.getNotifyType().getName(), - config.getNotifierId(), - config.getTemplateId()); - }); - } -} diff --git a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/rule/NotifierTaskExecutorProvider.java b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/rule/NotifierTaskExecutorProvider.java new file mode 100644 index 00000000..c716d8b4 --- /dev/null +++ b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/rule/NotifierTaskExecutorProvider.java @@ -0,0 +1,66 @@ +package org.jetlinks.community.notify.rule; + +import lombok.AllArgsConstructor; +import org.hswebframework.web.bean.FastBeanCopier; +import org.jetlinks.community.notify.NotifierManager; +import org.jetlinks.core.Values; +import org.jetlinks.rule.engine.api.RuleData; +import org.jetlinks.rule.engine.api.RuleDataHelper; +import org.jetlinks.rule.engine.api.task.ExecutionContext; +import org.jetlinks.rule.engine.api.task.TaskExecutor; +import org.jetlinks.rule.engine.api.task.TaskExecutorProvider; +import org.jetlinks.rule.engine.defaults.FunctionTaskExecutor; +import org.reactivestreams.Publisher; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.util.function.Function; + +@Component +@AllArgsConstructor +public class NotifierTaskExecutorProvider implements TaskExecutorProvider { + + private final NotifierManager notifierManager; + + @Override + public String getExecutor() { + return "notifier"; + } + + @Override + public Mono createTask(ExecutionContext context) { + RuleNotifierProperties properties = FastBeanCopier.copy(context.getJob().getConfiguration(), RuleNotifierProperties.class); + properties.validate(); + + Function> executor = createExecutor(context, properties); + return Mono.just(new FunctionTaskExecutor("消息通知", context) { + @Override + protected Publisher apply(RuleData input) { + return executor.apply(input); + } + }); + } + + + public Function> createExecutor(ExecutionContext context, RuleNotifierProperties config) { + return rule -> notifierManager + .getNotifier(config.getNotifyType(), config.getNotifierId()) + .switchIfEmpty(Mono.fromRunnable(() -> { + context.getLogger().warn("通知器[{}-{}]不存在", config.getNotifyType(), config.getNotifierId()); + })) + .flatMap(notifier -> notifier.send(config.getTemplateId(), Values.of(RuleDataHelper.toContextMap(rule)))) + .doOnError(err -> { + context.getLogger().error("发送[{}]通知[{}-{}]失败", + config.getNotifyType().getName(), + config.getNotifierId(), + config.getTemplateId(), err); + }) + .doOnSuccess(ignore -> { + context.getLogger().info("发送[{}]通知[{}-{}]完成", + config.getNotifyType().getName(), + config.getNotifierId(), + config.getTemplateId()); + }).then(Mono.empty()); + } + +} diff --git a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/rule/RuleNotifierProperties.java b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/rule/RuleNotifierProperties.java index 295af470..7674c935 100644 --- a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/rule/RuleNotifierProperties.java +++ b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/rule/RuleNotifierProperties.java @@ -3,13 +3,11 @@ package org.jetlinks.community.notify.rule; import lombok.Getter; import lombok.Setter; import org.jetlinks.community.notify.DefaultNotifyType; -import org.jetlinks.rule.engine.api.model.NodeType; -import org.jetlinks.rule.engine.executor.node.RuleNodeConfig; import org.springframework.util.Assert; @Getter @Setter -public class RuleNotifierProperties implements RuleNodeConfig { +public class RuleNotifierProperties { private DefaultNotifyType notifyType; @@ -17,17 +15,6 @@ public class RuleNotifierProperties implements RuleNodeConfig { private String templateId; - @Override - public NodeType getNodeType() { - return NodeType.PEEK; - } - - @Override - public void setNodeType(NodeType nodeType) { - - } - - @Override public void validate() { Assert.notNull(notifyType,"notifyType can not be null"); Assert.hasText(notifierId,"notifierId can not be empty"); diff --git a/jetlinks-components/rule-engine-component/pom.xml b/jetlinks-components/rule-engine-component/pom.xml index d35f24a7..6fd6705a 100644 --- a/jetlinks-components/rule-engine-component/pom.xml +++ b/jetlinks-components/rule-engine-component/pom.xml @@ -13,6 +13,11 @@ rule-engine-component + + com.cronutils + cron-utils + 9.0.2 + org.jetlinks rule-engine-support diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/configuration/EventTopicMessage.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/configuration/EventTopicMessage.java new file mode 100644 index 00000000..884915f9 --- /dev/null +++ b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/configuration/EventTopicMessage.java @@ -0,0 +1,42 @@ +package org.jetlinks.community.rule.engine.configuration; + +import io.netty.buffer.ByteBuf; +import lombok.Getter; +import lombok.Setter; +import org.jetlinks.core.message.codec.EncodedMessage; +import org.jetlinks.community.gateway.EncodableMessage; +import org.jetlinks.community.gateway.TopicMessage; +import org.jetlinks.rule.engine.api.NativePayload; +import org.jetlinks.rule.engine.api.SubscribePayload; + +import javax.annotation.Nonnull; + +@Getter +@Setter +public class EventTopicMessage implements TopicMessage, EncodableMessage { + private String topic; + + private Object nativePayload; + + private SubscribePayload payload; + + public EventTopicMessage(SubscribePayload payload) { + this.topic = payload.getTopic(); + this.nativePayload = ((NativePayload) payload.getPayload()).getNativeObject(); + this.payload = payload; + } + + @Nonnull + @Override + public ByteBuf getPayload() { + return payload.getBody(); + } + + @Nonnull + @Override + public EncodedMessage getMessage() { + return this; + } + + +} \ No newline at end of file diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineConfiguration.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineConfiguration.java index 4ed5ae10..a85e5812 100644 --- a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineConfiguration.java +++ b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineConfiguration.java @@ -1,33 +1,36 @@ package org.jetlinks.community.rule.engine.configuration; -import org.jetlinks.community.rule.engine.nodes.TimerWorkerNode; -import org.jetlinks.rule.engine.api.ConditionEvaluator; +import lombok.extern.slf4j.Slf4j; +import org.jetlinks.community.gateway.MessageGateway; +import org.jetlinks.rule.engine.api.EventBus; import org.jetlinks.rule.engine.api.RuleEngine; -import org.jetlinks.rule.engine.api.Slf4jLogger; -import org.jetlinks.rule.engine.api.executor.ExecutableRuleNodeFactory; -import org.jetlinks.rule.engine.cluster.logger.ClusterLogger; +import org.jetlinks.rule.engine.api.rpc.RpcService; +import org.jetlinks.rule.engine.api.rpc.RpcServiceFactory; +import org.jetlinks.rule.engine.api.scheduler.Scheduler; +import org.jetlinks.rule.engine.api.task.ConditionEvaluator; +import org.jetlinks.rule.engine.api.task.TaskExecutorProvider; +import org.jetlinks.rule.engine.api.worker.Worker; import org.jetlinks.rule.engine.condition.ConditionEvaluatorStrategy; import org.jetlinks.rule.engine.condition.DefaultConditionEvaluator; import org.jetlinks.rule.engine.condition.supports.DefaultScriptEvaluator; import org.jetlinks.rule.engine.condition.supports.ScriptConditionEvaluatorStrategy; import org.jetlinks.rule.engine.condition.supports.ScriptEvaluator; -import org.jetlinks.rule.engine.executor.DefaultExecutableRuleNodeFactory; -import org.jetlinks.rule.engine.executor.ExecutableRuleNodeFactoryStrategy; -import org.jetlinks.rule.engine.executor.node.route.RouteEventNode; +import org.jetlinks.rule.engine.defaults.DefaultRuleEngine; +import org.jetlinks.rule.engine.defaults.LocalEventBus; +import org.jetlinks.rule.engine.defaults.LocalScheduler; +import org.jetlinks.rule.engine.defaults.LocalWorker; +import org.jetlinks.rule.engine.defaults.rpc.DefaultRpcServiceFactory; +import org.jetlinks.rule.engine.defaults.rpc.EventBusRcpService; import org.jetlinks.rule.engine.model.DefaultRuleModelParser; import org.jetlinks.rule.engine.model.RuleModelParserStrategy; import org.jetlinks.rule.engine.model.antv.AntVG6RuleModelParserStrategy; -import org.jetlinks.rule.engine.standalone.StandaloneRuleEngine; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.boot.CommandLineRunner; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import java.util.concurrent.ExecutorService; - @Configuration +@Slf4j public class RuleEngineConfiguration { @Bean @@ -40,20 +43,46 @@ public class RuleEngineConfiguration { return new DefaultConditionEvaluator(); } - @Bean - public DefaultExecutableRuleNodeFactory defaultExecutableRuleNodeFactory() { - return new DefaultExecutableRuleNodeFactory(); - } - @Bean public AntVG6RuleModelParserStrategy antVG6RuleModelParserStrategy() { return new AntVG6RuleModelParserStrategy(); } + @Bean + public EventBus eventBus(MessageGateway messageGateway) { + + LocalEventBus local = new LocalEventBus(); + + //转发到消息网关 + local.subscribe("/**") + .flatMap(subscribePayload -> messageGateway.publish(new EventTopicMessage(subscribePayload)).then()) + .onErrorContinue((err, obj) -> log.error(err.getMessage(), obj)) + .subscribe(); + + return local; + } + + @Bean + public RpcService rpcService(EventBus eventBus) { + return new EventBusRcpService(eventBus); + } + + @Bean + public RpcServiceFactory rpcServiceFactory(RpcService rpcService) { + return new DefaultRpcServiceFactory(rpcService); + } + + @Bean + public Scheduler localScheduler(Worker worker) { + LocalScheduler scheduler = new LocalScheduler("local"); + scheduler.addWorker(worker); + return scheduler; + } + @Bean public BeanPostProcessor autoRegisterStrategy(DefaultRuleModelParser defaultRuleModelParser, DefaultConditionEvaluator defaultConditionEvaluator, - DefaultExecutableRuleNodeFactory ruleNodeFactory) { + LocalWorker worker) { return new BeanPostProcessor() { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { @@ -69,9 +98,10 @@ public class RuleEngineConfiguration { if (bean instanceof ConditionEvaluatorStrategy) { defaultConditionEvaluator.register(((ConditionEvaluatorStrategy) bean)); } - if (bean instanceof ExecutableRuleNodeFactoryStrategy) { - ruleNodeFactory.registerStrategy(((ExecutableRuleNodeFactoryStrategy) bean)); + if (bean instanceof TaskExecutorProvider) { + worker.addExecutor(((TaskExecutorProvider) bean)); } + return bean; } }; @@ -88,37 +118,14 @@ public class RuleEngineConfiguration { } @Bean - public RuleEngine ruleEngine(ExecutableRuleNodeFactory ruleNodeFactory, - ConditionEvaluator conditionEvaluator, - ApplicationEventPublisher eventPublisher, - ExecutorService executorService) { - StandaloneRuleEngine ruleEngine = new StandaloneRuleEngine(); - ruleEngine.setNodeFactory(ruleNodeFactory); - ruleEngine.setExecutor(executorService); - ruleEngine.setEvaluator(conditionEvaluator); - ruleEngine.setEventListener(eventPublisher::publishEvent); - ruleEngine.setLoggerSupplier((ctxId, model) -> { - ClusterLogger logger = new ClusterLogger(); - logger.setParent(new Slf4jLogger("rule.engine.logger.".concat(model.getId()).concat(".").concat(model.getName()))); - logger.setLogInfoConsumer(eventPublisher::publishEvent); - logger.setNodeId(model.getId()); - logger.setInstanceId(ctxId); - return logger; - }); - return ruleEngine; + public LocalWorker localWorker(EventBus eventBus, ConditionEvaluator evaluator) { + return new LocalWorker("local", "local", eventBus, evaluator); } - /* 规则引擎节点 */ - - @Bean //定时调度 - public TimerWorkerNode timerWorkerNode() { - return new TimerWorkerNode(); - } @Bean - public RouteEventNode routeEventNode() { - return new RouteEventNode(); + public RuleEngine defaultRuleEngine(Scheduler scheduler) { + return new DefaultRuleEngine(scheduler); } - } diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/event/handler/RuleLogHandler.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/event/handler/RuleLogHandler.java deleted file mode 100644 index 4b56cabb..00000000 --- a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/event/handler/RuleLogHandler.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.jetlinks.community.rule.engine.event.handler; - -import lombok.extern.slf4j.Slf4j; -import org.hswebframework.web.bean.FastBeanCopier; -import org.jetlinks.community.elastic.search.service.ElasticSearchService; -import org.jetlinks.community.gateway.MessageGateway; -import org.jetlinks.community.rule.engine.entity.RuleEngineExecuteEventInfo; -import org.jetlinks.community.rule.engine.entity.RuleEngineExecuteLogInfo; -import org.jetlinks.rule.engine.api.events.NodeExecuteEvent; -import org.jetlinks.rule.engine.api.events.RuleEvent; -import org.jetlinks.rule.engine.cluster.logger.LogInfo; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.event.EventListener; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; - -@Component -@Slf4j -@Order(3) -public class RuleLogHandler { - - @Autowired - private ElasticSearchService elasticSearchService; - - @Autowired - private MessageGateway messageGateway; - - @EventListener - public void handleRuleLog(LogInfo event) { - RuleEngineExecuteLogInfo logInfo = FastBeanCopier.copy(event, new RuleEngineExecuteLogInfo()); - elasticSearchService.commit(RuleEngineLoggerIndexProvider.RULE_LOG, logInfo) - .subscribe(); - // /rule-engine/{instanceId}/{nodeId}/log - messageGateway - .publish(String.join("/", - "/rule-engine", - event.getInstanceId(), - event.getNodeId(), - "log"), logInfo, true) - .subscribe(); - } - - @EventListener - public void handleRuleExecuteEvent(NodeExecuteEvent event) { - //不记录BEFORE和RESULT事件 - if (!RuleEvent.NODE_EXECUTE_BEFORE.equals(event.getEvent()) - && !RuleEvent.NODE_EXECUTE_RESULT.equals(event.getEvent())) { - RuleEngineExecuteEventInfo eventInfo = FastBeanCopier.copy(event, new RuleEngineExecuteEventInfo()); - elasticSearchService.commit(RuleEngineLoggerIndexProvider.RULE_EVENT_LOG, eventInfo) - .subscribe(); - } - // /rule-engine/{instanceId}/{nodeId}/event/{eventType} - messageGateway - .publish(String.join("/", - "/rule-engine", - event.getInstanceId(), - event.getNodeId(), - "event", - event.getEvent().toLowerCase() - ), event, true) - .subscribe(); - } - -} diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/DataMappingWorkerNode.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/DataMappingTaskExecutorProvider.java similarity index 78% rename from jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/DataMappingWorkerNode.java rename to jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/DataMappingTaskExecutorProvider.java index 30d079e9..3f8cb2e7 100644 --- a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/DataMappingWorkerNode.java +++ b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/DataMappingTaskExecutorProvider.java @@ -1,4 +1,4 @@ -package org.jetlinks.community.rule.engine.nodes; +package org.jetlinks.community.rule.engine.executor; import lombok.Getter; import lombok.Setter; @@ -6,18 +6,15 @@ import lombok.SneakyThrows; import org.hswebframework.web.bean.Converter; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.utils.ExpressionUtils; -import org.jetlinks.rule.engine.api.RuleData; -import org.jetlinks.rule.engine.api.executor.ExecutionContext; -import org.jetlinks.rule.engine.api.model.NodeType; -import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy; -import org.jetlinks.rule.engine.executor.node.RuleNodeConfig; -import org.reactivestreams.Publisher; +import org.jetlinks.rule.engine.api.task.ExecutionContext; +import org.jetlinks.rule.engine.api.task.TaskExecutor; +import org.jetlinks.rule.engine.api.task.TaskExecutorProvider; +import org.jetlinks.rule.engine.defaults.LambdaTaskExecutor; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; import java.math.BigDecimal; import java.util.*; -import java.util.function.Function; import java.util.stream.Collectors; /** @@ -25,31 +22,36 @@ import java.util.stream.Collectors; * @since 1.0.0 */ @Component -public class DataMappingWorkerNode extends CommonExecutableRuleNodeFactoryStrategy { +public class DataMappingTaskExecutorProvider implements TaskExecutorProvider { public static Converter converter = FastBeanCopier.DEFAULT_CONVERT; @Override - public String getSupportType() { + public String getExecutor() { return "data-mapping"; } @Override - public Function> createExecutor(ExecutionContext context, Config config) { + public Mono createTask(ExecutionContext context) { - return ruleData -> Mono.just(config.mapping(convertObject(ruleData.getData()))); + return Mono.just(new LambdaTaskExecutor("Mapping", context, () -> { + + Config config = FastBeanCopier.copy(context.getJob().getConfiguration(), new Config()); + + return data -> Mono.just(data.newData(config.mapping(data.getData()))); + + })); } + @Getter @Setter - public static class Config implements RuleNodeConfig { + public static class Config { private List mappings = new ArrayList<>(); private boolean keepSourceData = false; - private NodeType nodeType; - private Map toMap(Object source) { return FastBeanCopier.copy(source, HashMap::new); } @@ -62,10 +64,10 @@ public class DataMappingWorkerNode extends CommonExecutableRuleNodeFactoryStrate if (data instanceof Collection) { Collection source = ((Collection) data); return source - .stream() - .map(this::toMap) - .map(this::doMapping) - .collect(Collectors.toList()); + .stream() + .map(this::toMap) + .map(this::doMapping) + .collect(Collectors.toList()); } return data; } diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/DeviceMessageSendNode.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/DeviceMessageSendTaskExecutorProvider.java similarity index 57% rename from jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/DeviceMessageSendNode.java rename to jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/DeviceMessageSendTaskExecutorProvider.java index 7fd8241b..af88b26d 100644 --- a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/DeviceMessageSendNode.java +++ b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/DeviceMessageSendTaskExecutorProvider.java @@ -1,22 +1,25 @@ -package org.jetlinks.community.rule.engine.nodes; +package org.jetlinks.community.rule.engine.executor; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; +import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.id.IDGenerator; import org.jetlinks.core.device.DeviceOperator; import org.jetlinks.core.device.DeviceProductOperator; import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.core.message.DeviceMessageReply; import org.jetlinks.core.message.Headers; import org.jetlinks.core.message.MessageType; import org.jetlinks.core.message.RepayableDeviceMessage; import org.jetlinks.rule.engine.api.RuleData; -import org.jetlinks.rule.engine.api.executor.ExecutionContext; -import org.jetlinks.rule.engine.api.model.NodeType; -import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy; -import org.jetlinks.rule.engine.executor.node.RuleNodeConfig; +import org.jetlinks.rule.engine.api.task.ExecutionContext; +import org.jetlinks.rule.engine.api.task.TaskExecutor; +import org.jetlinks.rule.engine.api.task.TaskExecutorProvider; +import org.jetlinks.rule.engine.defaults.FunctionTaskExecutor; import org.reactivestreams.Publisher; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -24,18 +27,35 @@ import reactor.core.scheduler.Schedulers; import java.util.HashMap; import java.util.Map; -import java.util.function.Function; -@AllArgsConstructor @Component -public class DeviceMessageSendNode extends CommonExecutableRuleNodeFactoryStrategy { +@AllArgsConstructor +public class DeviceMessageSendTaskExecutorProvider implements TaskExecutorProvider { private final DeviceRegistry registry; @Override - public Function> createExecutor(ExecutionContext context, Config config) { + public String getExecutor() { + return "device-message-sender"; + } - return data -> { + @Override + public Mono createTask(ExecutionContext context) { + return Mono.just(new DeviceMessageSendTaskExecutor(context)); + } + + class DeviceMessageSendTaskExecutor extends FunctionTaskExecutor { + + private Config config; + + public DeviceMessageSendTaskExecutor(ExecutionContext context) { + super("发送设备消息", context); + validate(); + reload(); + } + + @Override + protected Publisher apply(RuleData input) { Flux devices = StringUtils.hasText(config.getDeviceId()) ? registry.getDevice(config.getDeviceId()).flux() : registry.getProduct(config.getProductId()).flatMapMany(DeviceProductOperator::getDevices); @@ -44,18 +64,32 @@ public class DeviceMessageSendNode extends CommonExecutableRuleNodeFactoryStrate .filterWhen(DeviceOperator::isOnline) .publishOn(Schedulers.parallel()) .flatMap(config::doSend) - .onErrorResume(error -> context.onError(data, error).then(Mono.empty())); - }; + .onErrorResume(error -> context.onError(error, input).then(Mono.empty())) + .map(reply -> input.newData(reply.toJson())) + ; + } + + @Override + public void validate() { + if (CollectionUtils.isEmpty(context.getJob().getConfiguration())) { + throw new IllegalArgumentException("配置不能为空"); + } + Config config = FastBeanCopier.copy(context.getJob().getConfiguration(), new Config()); + config.validate(); + } + + @Override + public void reload() { + config = FastBeanCopier.copy(context.getJob().getConfiguration(), new Config()); + } + + } - @Override - public String getSupportType() { - return "device-message-sender"; - } @Getter @Setter - public static class Config implements RuleNodeConfig { + public static class Config { //设备ID private String deviceId; @@ -67,7 +101,8 @@ public class DeviceMessageSendNode extends CommonExecutableRuleNodeFactoryStrate private boolean async; - public Publisher doSend(DeviceOperator device) { + @SuppressWarnings("all") + public Publisher doSend(DeviceOperator device) { Map message = new HashMap<>(this.message); message.put("messageId", IDGenerator.SNOW_FLAKE_STRING.generate()); message.put("deviceId", device.getDeviceId()); @@ -78,7 +113,6 @@ public class DeviceMessageSendNode extends CommonExecutableRuleNodeFactoryStrate .flatMapMany(msg -> device.messageSender().send(Mono.just(msg))); } - @Override public void validate() { if (StringUtils.isEmpty(deviceId) && StringUtils.isEmpty(productId)) { throw new IllegalArgumentException("deviceId和productId不能同时为空"); @@ -86,15 +120,5 @@ public class DeviceMessageSendNode extends CommonExecutableRuleNodeFactoryStrate MessageType.convertMessage(message).orElseThrow(() -> new IllegalArgumentException("不支持的消息格式")); } - @Override - public NodeType getNodeType() { - return NodeType.MAP; - } - - @Override - public void setNodeType(NodeType nodeType) { - - } } - } diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/ReactorQLTaskExecutorProvider.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/ReactorQLTaskExecutorProvider.java new file mode 100644 index 00000000..050f325e --- /dev/null +++ b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/ReactorQLTaskExecutorProvider.java @@ -0,0 +1,125 @@ +package org.jetlinks.community.rule.engine.executor; + +import com.alibaba.fastjson.JSONObject; +import lombok.AllArgsConstructor; +import org.jetlinks.community.gateway.MessageGateway; +import org.jetlinks.community.gateway.Subscription; +import org.jetlinks.community.gateway.TopicMessage; +import org.jetlinks.reactor.ql.ReactorQL; +import org.jetlinks.rule.engine.api.RuleConstants; +import org.jetlinks.rule.engine.api.RuleData; +import org.jetlinks.rule.engine.api.RuleDataHelper; +import org.jetlinks.rule.engine.api.model.RuleNodeModel; +import org.jetlinks.rule.engine.api.task.ExecutionContext; +import org.jetlinks.rule.engine.api.task.TaskExecutor; +import org.jetlinks.rule.engine.api.task.TaskExecutorProvider; +import org.jetlinks.rule.engine.defaults.AbstractTaskExecutor; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Component +@AllArgsConstructor +public class ReactorQLTaskExecutorProvider implements TaskExecutorProvider { + + private final MessageGateway messageGateway; + + @Override + public String getExecutor() { + return "reactor-ql"; + } + + @Override + public Mono createTask(ExecutionContext context) { + return Mono.just(new ReactorQLTaskExecutor(context)); + } + + class ReactorQLTaskExecutor extends AbstractTaskExecutor { + + private ReactorQL reactorQL; + + public ReactorQLTaskExecutor(ExecutionContext context) { + super(context); + reactorQL = createQl(); + } + + @Override + public String getName() { + return "ReactorQL"; + } + + @Override + protected Disposable doStart() { + Disposable.Composite composite = Disposables.composite(); + Flux> dataStream; + //有上游节点 + if (!CollectionUtils.isEmpty(context.getJob().getInputs())) { + + dataStream = context.getInput() + .accept() + .map(RuleDataHelper::toContextMap) + .as(reactorQL::start) + ; + } else { + dataStream = reactorQL + .start(table -> { + if (table == null || table.equalsIgnoreCase("dual")) { + return Flux.just(1); + } + if (table.startsWith("/")) { + //转换为消息 + return messageGateway + .subscribe( + Collections.singleton(new Subscription(table)), + "rule-engine:".concat(context.getInstanceId()), + false) + .map(TopicMessage::convertMessage); + } + return Flux.just(1); + }); + } + + return dataStream + .flatMap(result -> { + RuleData data = context.newRuleData(result); + //输出到下一节点 + return context.getOutput() + .write(Mono.just(data)) + .then(context.fireEvent(RuleConstants.Event.result, data)); + }) + .onErrorResume(err -> context.onError(err, null)) + .subscribe(); + } + + protected ReactorQL createQl() { + ReactorQL.Builder builder = Optional.ofNullable(context.getJob().getConfiguration()) + .map(map -> map.get("sql")) + .map(String::valueOf) + .map(ReactorQL.builder()::sql) + .orElseThrow(() -> new IllegalArgumentException("配置sql错误")); + return builder.build(); + } + + @Override + public void reload() { + reactorQL = createQl(); + if (this.disposable != null) { + this.disposable.dispose(); + } + start(); + } + + @Override + public void validate() { + createQl(); + } + } +} diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/ScriptWorkerNode.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/ScriptTaskExecutorProvider.java similarity index 70% rename from jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/ScriptWorkerNode.java rename to jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/ScriptTaskExecutorProvider.java index 7f7c75bf..804f35a4 100644 --- a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/ScriptWorkerNode.java +++ b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/ScriptTaskExecutorProvider.java @@ -1,4 +1,4 @@ -package org.jetlinks.community.rule.engine.nodes; +package org.jetlinks.community.rule.engine.executor; import lombok.Getter; import lombok.Setter; @@ -7,11 +7,13 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; import org.hswebframework.expands.script.engine.DynamicScriptEngine; import org.hswebframework.expands.script.engine.DynamicScriptEngineFactory; +import org.hswebframework.web.bean.FastBeanCopier; import org.jetlinks.rule.engine.api.RuleData; -import org.jetlinks.rule.engine.api.executor.ExecutionContext; -import org.jetlinks.rule.engine.api.model.NodeType; -import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy; -import org.jetlinks.rule.engine.executor.node.RuleNodeConfig; +import org.jetlinks.rule.engine.api.model.RuleNodeModel; +import org.jetlinks.rule.engine.api.task.ExecutionContext; +import org.jetlinks.rule.engine.api.task.TaskExecutor; +import org.jetlinks.rule.engine.api.task.TaskExecutorProvider; +import org.jetlinks.rule.engine.defaults.LambdaTaskExecutor; import org.reactivestreams.Publisher; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -24,14 +26,22 @@ import java.util.function.Function; @Component @Slf4j -public class ScriptWorkerNode extends CommonExecutableRuleNodeFactoryStrategy { +public class ScriptTaskExecutorProvider implements TaskExecutorProvider { @Override - public String getSupportType() { + public String getExecutor() { return "script"; } @Override + public Mono createTask(ExecutionContext context) { + return Mono.just(new LambdaTaskExecutor("script", context, () -> { + + return createExecutor(context, FastBeanCopier.copy(context.getJob().getConfiguration(), new Config())); + + })); + } + @SneakyThrows public Function> createExecutor(ExecutionContext context, Config config) { @@ -54,16 +64,16 @@ public class ScriptWorkerNode extends CommonExecutableRuleNodeFactoryStrategy Flux.defer(()->{ + return ruleData -> Flux.defer(() -> { if (handler.onMessage != null) { Object result = handler.onMessage.apply(ruleData); if (result == null || result.getClass().getName().equals("jdk.nashorn.internal.runtime.Undefined")) { return Flux.empty(); } - if(result instanceof Publisher){ - return Flux.from(((Publisher) result)); + if (result instanceof Publisher) { + return Flux.from(((Publisher) result)); } - if(result instanceof Map){ + if (result instanceof Map) { result = new HashMap<>((Map) result); } return Flux.just(result); @@ -82,13 +92,11 @@ public class ScriptWorkerNode extends CommonExecutableRuleNodeFactoryStrategy { +public class SqlExecutorTaskExecutorProvider implements TaskExecutorProvider { @Autowired private ReactiveSqlExecutor sqlExecutor; - @Override - public String getSupportType() { + public String getExecutor() { return "sql"; } - @Override - public Function> createExecutor(ExecutionContext context, Config config) { + public Function> createExecutor(ExecutionContext context, Config config) { if (config.isQuery()) { return (data) -> Flux.defer(() -> { String sql = config.getSql(data); List>> fluxes = new ArrayList<>(); data.acceptMap(map -> fluxes.add(sqlExecutor.select(Mono.just(SqlRequests.template(sql, map)), ResultWrappers.map()))); - return Flux.concat(fluxes) ; + return Flux.concat(fluxes); }); } else { return data -> Mono.defer(() -> { @@ -57,10 +57,15 @@ public class SqlExecutorWorkerNode extends CommonExecutableRuleNodeFactoryStrate } + @Override + public Mono createTask(ExecutionContext context) { + return Mono.just(new LambdaTaskExecutor("SQL",context, () -> createExecutor(context, FastBeanCopier.copy(context.getJob().getConfiguration(),new Config())))); + } + @Getter @Setter - public static class Config implements RuleNodeConfig { + public static class Config { private String dataSourceId; @@ -75,7 +80,7 @@ public class SqlExecutorWorkerNode extends CommonExecutableRuleNodeFactoryStrate public boolean isQuery() { return sql.trim().startsWith("SELECT") || - sql.trim().startsWith("select"); + sql.trim().startsWith("select"); } @SneakyThrows @@ -83,20 +88,10 @@ public class SqlExecutorWorkerNode extends CommonExecutableRuleNodeFactoryStrate if (!sql.contains("${")) { return sql; } - Map map = new HashMap<>(); - map.put("data", data.getData()); - map.put("ruleData", data); - map.put("attr", data.getAttributes()); - return ExpressionUtils.analytical(sql, map, "spel"); + + return ExpressionUtils.analytical(sql, RuleDataHelper.toContextMap(data), "spel"); } - public void switchDataSource() { - - } - - public void resetDataSource() { - - } } } diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/TimerTaskExecutorProvider.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/TimerTaskExecutorProvider.java new file mode 100644 index 00000000..abfc71c0 --- /dev/null +++ b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/TimerTaskExecutorProvider.java @@ -0,0 +1,125 @@ +package org.jetlinks.community.rule.engine.executor; + +import com.cronutils.model.Cron; +import com.cronutils.model.CronType; +import com.cronutils.model.definition.CronDefinitionBuilder; +import com.cronutils.model.time.ExecutionTime; +import com.cronutils.parser.CronParser; +import lombok.AllArgsConstructor; +import org.jetlinks.community.ValueObject; +import org.jetlinks.rule.engine.api.RuleConstants; +import org.jetlinks.rule.engine.api.task.ExecutionContext; +import org.jetlinks.rule.engine.api.task.TaskExecutor; +import org.jetlinks.rule.engine.api.task.TaskExecutorProvider; +import org.jetlinks.rule.engine.defaults.AbstractTaskExecutor; +import org.springframework.stereotype.Component; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +import java.time.Duration; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; +import java.util.function.Supplier; + +@Component +@AllArgsConstructor +public class TimerTaskExecutorProvider implements TaskExecutorProvider { + + private final Scheduler scheduler; + + @Override + public String getExecutor() { + return "timer"; + } + + @Override + public Mono createTask(ExecutionContext context) { + return Mono.just(new TimerTaskExecutor(context)); + } + + class TimerTaskExecutor extends AbstractTaskExecutor { + + Supplier nextDelay; + + public TimerTaskExecutor(ExecutionContext context) { + super(context); + nextDelay = createNextDelay(); + } + + @Override + public String getName() { + return "定时调度"; + } + + @Override + protected Disposable doStart() { + return execute(); + } + + private Disposable execute() { + Duration nextTime = nextDelay.get(); + context.getLogger().debug("trigger timed task after {}", nextTime); + if (this.disposable != null) { + this.disposable.dispose(); + } + return this.disposable = + Mono.delay(nextTime, scheduler) + .flatMap(t -> context.getOutput().write(Mono.just(context.newRuleData(t)))) + .then(context.fireEvent(RuleConstants.Event.complete, context.newRuleData(System.currentTimeMillis())).thenReturn(1)) + .subscribe(t -> execute()); + } + + @Override + public void reload() { + nextDelay = createNextDelay(); + if (disposable != null) { + disposable.dispose(); + } + doStart(); + } + + @Override + public void validate() { + createNextDelay(); + } + + private Supplier createNextDelay() { + ValueObject config = ValueObject.of(context.getJob().getConfiguration()); + + CronParser parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ)); + Cron cron = config.getString("cron") + .map(parser::parse) + .orElseThrow(() -> new IllegalArgumentException("cron配置不存在")); + ExecutionTime executionTime = ExecutionTime.forCron(cron); + + return () -> executionTime.timeToNextExecution(ZonedDateTime.now()).orElse(Duration.ofSeconds(10)); + + } + + } + + public static Flux getLastExecuteTimes(String cronExpression, Date from, long times) { + return Flux.create(sink -> { + CronParser parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ)); + Cron cron = parser.parse(cronExpression); + ExecutionTime executionTime = ExecutionTime.forCron(cron); + ZonedDateTime dateTime = ZonedDateTime.ofInstant(from.toInstant(), ZoneId.systemDefault()); + + for (long i = 0; i < times; i++) { + dateTime = executionTime.nextExecution(dateTime) + .orElse(null); + if (dateTime != null) { + sink.next(dateTime); + } else { + break; + } + } + sink.complete(); + + + }); + } +} diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/ReactorSqlNode.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/ReactorSqlNode.java deleted file mode 100644 index 45612e27..00000000 --- a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/ReactorSqlNode.java +++ /dev/null @@ -1,134 +0,0 @@ -package org.jetlinks.community.rule.engine.nodes; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import org.jetlinks.core.message.codec.MessagePayloadType; -import org.jetlinks.community.gateway.EncodableMessage; -import org.jetlinks.community.gateway.MessageGateway; -import org.jetlinks.community.gateway.Subscription; -import org.jetlinks.reactor.ql.ReactorQL; -import org.jetlinks.rule.engine.api.RuleData; -import org.jetlinks.rule.engine.api.RuleDataHelper; -import org.jetlinks.rule.engine.api.events.RuleEvent; -import org.jetlinks.rule.engine.api.executor.ExecutionContext; -import org.jetlinks.rule.engine.api.model.NodeType; -import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy; -import org.jetlinks.rule.engine.executor.PayloadType; -import org.jetlinks.rule.engine.executor.node.RuleNodeConfig; -import org.reactivestreams.Publisher; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.util.Collections; -import java.util.function.Function; - -/** - *
- *     {@code
- *
- *     select avg(this.temperature) avgVal, deviceId
- *     from "/device/+/message/property/#"
- *     group _window(10,1) --每10条滚动数据
- *     having avgVal > 10
- *
- *     }
- * 
- */ -@Slf4j -@AllArgsConstructor -@Component -public class ReactorSqlNode extends CommonExecutableRuleNodeFactoryStrategy { - - private final MessageGateway messageGateway; - - @Override - public String getSupportType() { - return "reactor-ql"; - } - - @Override - public Function> createExecutor(ExecutionContext context, Config config) { - ReactorQL ql = config.getReactorQL(); - - return data -> ql.start(Flux.just(RuleDataHelper.toContextMap(data))); - } - - @Override - protected void onStarted(ExecutionContext context, Config config) { - log.debug("start reactor ql : {}", config.getSql()); - context.onStop( - config.getReactorQL() - .start(table -> { - if (table == null || table.equalsIgnoreCase("dual")) { - return Flux.just(1); - } - - if (table.startsWith("/")) { - return messageGateway - .subscribe( - Collections.singleton(new Subscription(table)), - "rule-engine:".concat(context.getInstanceId()), - false) - .map(msg -> { - //转换为消息 - if (msg.getMessage() instanceof EncodableMessage) { - return ((EncodableMessage) msg.getMessage()).getNativePayload(); - } - MessagePayloadType payloadType = msg.getMessage().getPayloadType(); - if (payloadType == null) { - return msg.getMessage().getBytes(); - } - return PayloadType.valueOf(payloadType.name()).read(msg.getMessage().getPayload()); - }); - } - return Flux.just(1); - }) - .flatMap(result -> { - RuleData data = RuleData.create(result); - //输出到下一节点 - return context.getOutput() - .write(Mono.just(RuleData.create(result))) - .then(context.fireEvent(RuleEvent.NODE_EXECUTE_DONE, data)); - }) - .onErrorResume(err -> context.onError(RuleData.create(""), err)) - .subscribe()::dispose - ); - - } - - public static class Config implements RuleNodeConfig { - - @Getter - @Setter - private String sql; - - private volatile ReactorQL reactorQL; - - @Override - public NodeType getNodeType() { - return NodeType.MAP; - } - - @Override - public void setNodeType(NodeType nodeType) { - - } - - public ReactorQL getReactorQL() { - if (reactorQL == null) { - reactorQL = ReactorQL.builder().sql(sql).build(); - } - return reactorQL; - } - - @Override - public void validate() { - //不报错就ok - getReactorQL(); - } - } - -} diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/TimerWorkerNode.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/TimerWorkerNode.java deleted file mode 100644 index 7cc44e1b..00000000 --- a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/TimerWorkerNode.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.jetlinks.community.rule.engine.nodes; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; -import org.jetlinks.rule.engine.api.RuleData; -import org.jetlinks.rule.engine.api.executor.ExecutionContext; -import org.jetlinks.rule.engine.api.model.NodeType; -import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy; -import org.jetlinks.rule.engine.executor.node.RuleNodeConfig; -import org.reactivestreams.Publisher; -import org.springframework.scheduling.support.CronSequenceGenerator; -import reactor.core.publisher.Mono; - -import java.time.Duration; -import java.util.Date; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; - -public class TimerWorkerNode extends CommonExecutableRuleNodeFactoryStrategy { - - private Map jobs = new ConcurrentHashMap<>(); - - @Override - public Function> createExecutor(ExecutionContext context, Configuration config) { - return Mono::just; - } - - @Override - protected void onStarted(ExecutionContext context, Configuration config) { - super.onStarted(context, config); - String id = context.getInstanceId() + ":" + context.getNodeId(); - - context.onStop(() -> { - TimerJob job = jobs.remove(id); - if (null != job) { - job.cancel(); - } - }); - TimerJob job = jobs.computeIfAbsent(id, _id -> new TimerJob(config, context)); - - job.start(); - } - - @Override - public String getSupportType() { - return "timer"; - } - - @AllArgsConstructor - private static class TimerJob { - private String id; - private TimerWorkerNode.Configuration configuration; - private ExecutionContext context; - private volatile boolean running; - - TimerJob(TimerWorkerNode.Configuration configuration, - ExecutionContext context) { - this.configuration = configuration; - this.context = context; - this.id = context.getInstanceId() + ":" + context.getNodeId(); - } - - - void start() { - running = true; - doStart(); - } - - void doStart() { - if (!running) { - return; - } - running = true; - Mono.delay(Duration.ofMillis(configuration.nextMillis())) - .subscribe(t -> execute(this::doStart)); - } - - void execute(Runnable runnable) { - if (!running) { - return; - } - context.logger().debug("execute timer:{}", id); - context.getOutput() - .write(Mono.just(RuleData.create(System.currentTimeMillis()))) - .doOnError(err -> context.logger().error("fire timer error", err)) - .doFinally(s -> runnable.run()) - .subscribe(); - } - - void cancel() { - running = false; - } - } - - - public static class Configuration implements RuleNodeConfig { - @Getter - @Setter - private String cron; - - private volatile CronSequenceGenerator generator; - - @Override - public NodeType getNodeType() { - return NodeType.PEEK; - } - - @Override - public void setNodeType(NodeType nodeType) { - - } - - public void init() { - generator = new CronSequenceGenerator(cron); - } - - @Override - public void validate() { - init(); - } - - public long nextMillis() { - return Math.max(100, generator.next(new Date()).getTime() - System.currentTimeMillis()); - } - - } -} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceCategory.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceCategory.java new file mode 100644 index 00000000..87d1e92f --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceCategory.java @@ -0,0 +1,24 @@ +package org.jetlinks.community.device.entity; + +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.api.crud.entity.GenericTreeSortSupportEntity; + +import java.util.List; + +@Getter +@Setter +public class DeviceCategory extends GenericTreeSortSupportEntity { + + private String id; + + private String key; + + private String name; + + private String parentId; + + private List children; + + +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceInstanceEntity.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceInstanceEntity.java index 3ae9d4cc..6b72054a 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceInstanceEntity.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceInstanceEntity.java @@ -49,10 +49,14 @@ public class DeviceInstanceEntity extends GenericEntity implements Recor private String describe; @Comment("产品id") - @Column(name = "product_id", length = 32) + @Column(name = "product_id", length = 64) @NotBlank(message = "产品ID不能为空", groups = CreateGroup.class) private String productId; + @Comment("图片地址") + @Column(name = "photo_url", length = 1024) + private String photoUrl; + @Comment("产品名称") @Column(name = "product_name") @NotBlank(message = "产品名称不能为空", groups = CreateGroup.class) @@ -89,11 +93,11 @@ public class DeviceInstanceEntity extends GenericEntity implements Recor @Column(name = "registry_time") private Long registryTime; - @Column(name = "org_id", length = 32) + @Column(name = "org_id", length = 64) @Comment("所属机构id") private String orgId; - @Column(name = "parent_id", length = 32) + @Column(name = "parent_id", length = 64) @Comment("父级设备ID") private String parentId; diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceProductEntity.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceProductEntity.java index 21eb6a74..a661177a 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceProductEntity.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceProductEntity.java @@ -40,9 +40,13 @@ public class DeviceProductEntity extends GenericEntity implements Record private String name; @Comment("所属项目") - @Column(name = "project_id",length = 32) + @Column(name = "project_id",length = 64) private String projectId; + @Comment("图片地址") + @Column(name = "photo_url", length = 1024) + private String photoUrl; + @Comment("项目名称") @Column(name = "project_name") private String projectName; @@ -52,9 +56,13 @@ public class DeviceProductEntity extends GenericEntity implements Record private String describe; @Comment("分类ID") - @Column(name = "classified_id") + @Column(name = "classified_id",length = 64) private String classifiedId; + @Column + @Comment("分类名称") + private String classifiedName; + @Comment("消息协议: Alink,JetLinks") @Column(name = "message_protocol") @NotBlank(message = "消息协议不能为空",groups = CreateGroup.class) @@ -63,6 +71,10 @@ public class DeviceProductEntity extends GenericEntity implements Record }) private String messageProtocol; + @Column + @Comment("协议名称") + private String protocolName; + @Comment("协议元数据") @Column(name = "metadata") @ColumnType(jdbcType = JDBCType.CLOB) diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceTagEntity.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceTagEntity.java index a0e668c1..867e065e 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceTagEntity.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceTagEntity.java @@ -60,6 +60,6 @@ public class DeviceTagEntity extends GenericEntity { } public static String createTagId(String deviceId,String key){ - return DigestUtils.md5Hex(deviceId.concat(":").concat(key)); + return DigestUtils.md5Hex(deviceId + ":" + key); } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceDetail.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceDetail.java index ad52927d..ec48720f 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceDetail.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceDetail.java @@ -27,9 +27,15 @@ public class DeviceDetail { //设备名称 private String name; + //设备图片 + private String photoUrl; + //消息协议标识 private String protocol; + //协议名称 + private String protocolName; + //通信协议 private String transport; @@ -132,10 +138,11 @@ public class DeviceDetail { } setProtocol(productEntity.getMessageProtocol()); setTransport(productEntity.getTransportProtocol()); - + setPhotoUrl(productEntity.getPhotoUrl()); setProductId(productEntity.getId()); setProductName(productEntity.getName()); setDeviceType(productEntity.getDeviceType()); + setProtocolName(productEntity.getProtocolName()); return this; } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceCategoryController.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceCategoryController.java new file mode 100644 index 00000000..6b5275f5 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceCategoryController.java @@ -0,0 +1,73 @@ +package org.jetlinks.community.device.web; + +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; +import org.hswebframework.web.api.crud.entity.TreeSupportEntity; +import org.jetlinks.community.device.entity.DeviceCategory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +@RestController +@RequestMapping("/device/category") +@Slf4j +public class DeviceCategoryController { + + + static List statics; + + + static void rebuild(String parentId, List children) { + if (children == null) { + return; + } + for (DeviceCategory child : children) { + String id = child.getId(); + child.setId(parentId + "|" + id + "|"); + child.setParentId(parentId + "|"); + rebuild(parentId + "|" + id, child.getChildren()); + } + } + + static { + try { + ClassPathResource resource = new ClassPathResource("device-category.json"); + String json = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8); + + List all = JSON.parseArray(json, DeviceCategory.class); + + List root = TreeSupportEntity.list2tree(all, DeviceCategory::setChildren); + + for (DeviceCategory category : root) { + String id = category.getId(); + + category.setId("|" + id + "|"); + category.setParentId("|" + category.getParentId()+"|"); + rebuild("|" + id, category.getChildren()); + } + + statics = all; + + } catch (Exception e) { + statics = new ArrayList<>(); + log.error(e.getMessage(), e); + } + } + + @GetMapping + public Flux getAllCategory() { + return Flux.fromIterable(statics); + } + + @GetMapping("/_tree") + public Flux getAllCategoryTree() { + return Flux.fromIterable(TreeSupportEntity.list2tree(statics, DeviceCategory::setChildren)); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceInstanceController.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceInstanceController.java index 1fa165a7..58e0c0f9 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceInstanceController.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceInstanceController.java @@ -369,14 +369,17 @@ public class DeviceInstanceController implements .flatMap(DeviceProductOperator::getMetadata) .map(metadata -> new DeviceWrapper(metadata.getTags())) .defaultIfEmpty(DeviceWrapper.empty) + .zipWith(productService.findById(productId)) .flatMapMany(wrapper -> importExportService .getInputStream(fileUrl) - .flatMapMany(inputStream -> ReactorExcel.read(inputStream, FileUtils.getExtension(fileUrl), wrapper))) + .flatMapMany(inputStream -> ReactorExcel.read(inputStream, FileUtils.getExtension(fileUrl), wrapper.getT1())) + .doOnNext(info -> info.setProductName(wrapper.getT2().getName())) + ) .map(info -> { DeviceInstanceEntity entity = FastBeanCopier.copy(info, new DeviceInstanceEntity()); entity.setProductId(productId); if (StringUtils.isEmpty(entity.getId())) { - throw new BusinessException("第" + info.getRowNumber() + 1 + "行:设备ID不能为空"); + throw new BusinessException("第" + (info.getRowNumber() + 1) + "行:设备ID不能为空"); } return Tuples.of(entity, info.getTags()); }) diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelInfo.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelInfo.java index 03c7c15a..95343c56 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelInfo.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelInfo.java @@ -33,7 +33,7 @@ public class DeviceExcelInfo { private long rowNumber; - public void tag(String key, String name, Object value) { + public void tag(String key, String name, Object value, String type) { if (value == null) { return; } @@ -41,14 +41,17 @@ public class DeviceExcelInfo { entity.setKey(key); entity.setValue(String.valueOf(value)); entity.setName(name); - entity.setId(String.valueOf(id).concat(":").concat(key)); + entity.setDeviceId(id); + entity.setType(type); + entity.setId(DeviceTagEntity.createTagId(id,key)); tags.add(entity); } public void setId(String id) { this.id = id; for (DeviceTagEntity tag : tags) { - tag.setId(String.valueOf(id).concat(":").concat(tag.getKey())); + tag.setDeviceId(id); + tag.setId(DeviceTagEntity.createTagId(tag.getDeviceId(),tag.getKey())); } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceWrapper.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceWrapper.java index f784f2f4..0194a27b 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceWrapper.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceWrapper.java @@ -18,14 +18,14 @@ import java.util.Map; */ public class DeviceWrapper extends RowWrapper { - Map tagMapping = new HashMap<>(); + Map tagMapping = new HashMap<>(); static Map headerMapping = DeviceExcelInfo.getImportHeaderMapping(); public static DeviceWrapper empty = new DeviceWrapper(Collections.emptyList()); public DeviceWrapper(List tags) { for (PropertyMetadata tag : tags) { - tagMapping.put(tag.getName(), tag.getId()); + tagMapping.put(tag.getName(), tag); } } @@ -38,9 +38,9 @@ public class DeviceWrapper extends RowWrapper { protected DeviceExcelInfo wrap(DeviceExcelInfo deviceExcelInfo, Cell header, Cell cell) { String headerText = header.valueAsText().orElse("null"); - String maybeTag = tagMapping.get(headerText); - if (StringUtils.hasText(maybeTag)) { - deviceExcelInfo.tag(maybeTag, headerText, cell.value().orElse(null)); + PropertyMetadata maybeTag = tagMapping.get(headerText); + if (maybeTag != null) { + deviceExcelInfo.tag(maybeTag.getId(), headerText, cell.value().orElse(null), maybeTag.getValueType().getId()); } else { deviceExcelInfo.with(headerMapping.getOrDefault(headerText, headerText), cell.value().orElse(null)); } diff --git a/jetlinks-manager/device-manager/src/main/resources/device-category.json b/jetlinks-manager/device-manager/src/main/resources/device-category.json new file mode 100644 index 00000000..991f0bbd --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/resources/device-category.json @@ -0,0 +1,2282 @@ +[ + { + "parentId": 0, + "key": "SmartCity", + "name": "智能城市", + "id": 1 + }, + { + "parentId": 1, + "key": "PublicService", + "name": "公共服务", + "id": 2 + }, + { + "parentId": 2, + "key": "Lighting", + "name": "路灯照明", + "id": 3 + }, + { + "parentId": 1, + "key": "EnergyManagement", + "name": "能源管理", + "id": 15 + }, + { + "parentId": 1, + "key": "EnvironmentalPerception", + "name": "环境感知", + "id": 16 + }, + { + "parentId": 1, + "key": "FireSafety", + "name": "消防安全", + "id": 17 + }, + { + "parentId": 1, + "key": "Farming", + "name": "种植养殖", + "id": 18 + }, + { + "parentId": 1, + "key": "SmartBuilding", + "name": "智能楼宇", + "id": 19 + }, + { + "parentId": 15, + "key": "ElectricityMeter", + "name": "电表", + "id": 20 + }, + { + "parentId": 15, + "key": "WaterMeter", + "name": "水表", + "id": 21 + }, + { + "parentId": 16, + "key": "AirConditioner", + "name": "空调", + "id": 22 + }, + { + "parentId": 16, + "key": "EnvironmentMonitor", + "name": "环境监测设备", + "id": 23 + }, + { + "parentId": 17, + "key": "SecurityGateway", + "name": "安防监测网关", + "id": 24 + }, + { + "parentId": 17, + "key": "SmokeDetector", + "name": "烟雾探测器", + "id": 25 + }, + { + "parentId": 19, + "key": "BlueToothScale", + "name": "蓝牙秤", + "id": 28 + }, + { + "parentId": 16, + "key": "ActivityDetectionEquipment", + "name": "活动检测设备", + "id": 31 + }, + { + "parentId": 19, + "key": "LightingFacility", + "name": "灯光设备", + "id": 32 + }, + { + "parentId": 16, + "key": "AirCollect", + "name": "大气监测设备", + "id": 33 + }, + { + "parentId": 17, + "key": "AcoustoOpticalarm", + "name": "声光报警设备", + "id": 34 + }, + { + "parentId": 19, + "key": "DrinkingFoundation", + "name": "饮水机", + "id": 35 + }, + { + "parentId": 18, + "key": "SmartSprinklerTerminal", + "name": "喷灌智能终端", + "id": 36 + }, + { + "parentId": 17, + "key": "ManholeCover", + "name": "井盖", + "id": 38 + }, + { + "parentId": 18, + "key": "AgriculturalMonitor", + "name": "农业监控设备", + "id": 40 + }, + { + "parentId": 0, + "key": "SmartLife", + "name": "智能生活", + "id": 41 + }, + { + "parentId": 41, + "key": "HomeSecurity", + "name": "家居安防", + "id": 42 + }, + { + "parentId": 42, + "key": "GasDetector", + "name": "燃气报警器", + "id": 43 + }, + { + "parentId": 42, + "key": "WaterDetector", + "name": "水浸报警器", + "id": 44 + }, + { + "parentId": 0, + "key": "SmartIndustry", + "name": "智能工业", + "id": 45 + }, + { + "parentId": 45, + "key": "TextileIndustry", + "name": "纺织业", + "id": 46 + }, + { + "parentId": 45, + "key": "PharmaceuticalManufacturing", + "name": "医药制造业", + "id": 47 + }, + { + "parentId": 45, + "key": "PlasticProductsIndustry", + "name": "塑料制品业", + "id": 48 + }, + { + "parentId": 45, + "key": "MetalProductsIndustry", + "name": "金属制品业", + "id": 49 + }, + { + "parentId": 45, + "key": "ChemicalFiberManufacturing", + "name": "化学纤维制造业", + "id": 50 + }, + { + "parentId": 42, + "key": "IRDetector", + "name": "红外探测器", + "id": 51 + }, + { + "parentId": 42, + "key": "DoorContact", + "name": "门磁传感器", + "id": 52 + }, + { + "parentId": 42, + "key": "SmokeAlarm", + "name": "烟雾报警器", + "id": 53 + }, + { + "parentId": 41, + "key": "Environment", + "name": "环境电器", + "id": 54 + }, + { + "parentId": 54, + "key": "Fan", + "name": "风扇", + "id": 55 + }, + { + "parentId": 54, + "key": "Humidifier", + "name": "加湿器", + "id": 56 + }, + { + "parentId": 54, + "key": "Heater", + "name": "取暖器", + "id": 57 + }, + { + "parentId": 41, + "key": "Electrical&Lighting", + "name": "电工照明", + "id": 58 + }, + { + "parentId": 58, + "key": "Light", + "name": "灯", + "id": 59 + }, + { + "parentId": 58, + "key": "Outlet", + "name": "插座", + "id": 60 + }, + { + "parentId": 58, + "key": "Curtain", + "name": "窗帘", + "id": 61 + }, + { + "parentId": 54, + "key": "Airbox", + "name": "空气盒子", + "id": 62 + }, + { + "parentId": 41, + "key": "MajorAppliance", + "name": "大家电", + "id": 63 + }, + { + "parentId": 63, + "key": "Fridge", + "name": "冰箱", + "id": 64 + }, + { + "parentId": 54, + "key": "Dehumidifier", + "name": "除湿器", + "id": 65 + }, + { + "parentId": 41, + "key": "KitchenAppliance", + "name": "厨房电器", + "id": 66 + }, + { + "parentId": 66, + "key": "KitchenVentilator", + "name": "油烟机", + "id": 67 + }, + { + "parentId": 41, + "key": "Health", + "name": "个护健康", + "id": 68 + }, + { + "parentId": 68, + "key": "FootBath", + "name": "足浴盆", + "id": 69 + }, + { + "parentId": 58, + "key": "WindowLinearActuator", + "name": "推窗器", + "id": 70 + }, + { + "parentId": 58, + "key": "WallSwitch", + "name": "入墙开关", + "id": 71 + }, + { + "parentId": 54, + "key": "FAU", + "name": "新风机", + "id": 72 + }, + { + "parentId": 68, + "key": "ElectricBlanket", + "name": "电热毯", + "id": 73 + }, + { + "parentId": 54, + "key": "FloorHeating", + "name": "地暖", + "id": 74 + }, + { + "parentId": 63, + "key": "ElectricWaterHeater", + "name": "电热水器", + "id": 75 + }, + { + "parentId": 2, + "key": "Locater", + "name": "车辆定位卡", + "id": 76 + }, + { + "parentId": 17, + "key": "LaserScanner", + "name": "激光探测仪", + "id": 77 + }, + { + "parentId": 41, + "key": "Others", + "name": "其它", + "id": 78 + }, + { + "parentId": 78, + "key": "BathHeater", + "name": "浴霸", + "id": 79 + }, + { + "parentId": 66, + "key": "Stove", + "name": "燃气灶", + "id": 80 + }, + { + "parentId": 63, + "key": "GasWaterHeater", + "name": "燃气热水器", + "id": 81 + }, + { + "parentId": 54, + "key": "WaterPurifier", + "name": "净水器", + "id": 82 + }, + { + "parentId": 54, + "key": "AirPurifier", + "name": "空气净化器", + "id": 83 + }, + { + "parentId": 63, + "key": "AirConditioning", + "name": "空调机", + "id": 84 + }, + { + "parentId": 68, + "key": "Wristband", + "name": "手环", + "id": 85 + }, + { + "parentId": 0, + "key": "LinkEdge", + "name": "边缘计算", + "id": 87 + }, + { + "parentId": 87, + "key": "gateway", + "name": "边缘网关", + "id": 88 + }, + { + "parentId": 87, + "key": "other", + "name": "其他设备", + "id": 89 + }, + { + "parentId": 68, + "key": "Bed", + "name": "床", + "id": 92 + }, + { + "parentId": 78, + "key": "Airer", + "name": "晾衣杆", + "id": 95 + }, + { + "parentId": 78, + "key": "Bathtub", + "name": "浴缸", + "id": 96 + }, + { + "parentId": 78, + "key": "ShowerRoom", + "name": "淋浴房", + "id": 97 + }, + { + "parentId": 78, + "key": "Sauna", + "name": "干蒸房", + "id": 98 + }, + { + "parentId": 78, + "key": "ToiletSeat", + "name": "马桶", + "id": 99 + }, + { + "parentId": 42, + "key": "WaterMonitor", + "name": "用水监控器", + "id": 100 + }, + { + "parentId": 0, + "key": "linkbusiness", + "name": "商业共享", + "id": 102 + }, + { + "parentId": 102, + "key": "retail", + "name": "零售设备", + "id": 103 + }, + { + "parentId": 102, + "key": "sharing", + "name": "共享租赁服务", + "id": 104 + }, + { + "parentId": 103, + "key": "Juicer", + "name": "果汁机", + "id": 105 + }, + { + "parentId": 41, + "key": "Networking", + "name": "网络设备", + "id": 106 + }, + { + "parentId": 106, + "key": "GeneralGateway", + "name": "网关", + "id": 107 + }, + { + "parentId": 103, + "key": "Water purifier", + "name": "净水机", + "id": 109 + }, + { + "parentId": 103, + "key": "Ice cream machine", + "name": "冰淇淋机", + "id": 110 + }, + { + "parentId": 103, + "key": "Coffee machine", + "name": "咖啡机", + "id": 111 + }, + { + "parentId": 103, + "key": "Containers", + "name": "售货柜", + "id": 112 + }, + { + "parentId": 103, + "key": "Grab the doll machine", + "name": "抓娃娃机", + "id": 113 + }, + { + "parentId": 104, + "key": "Massage chair", + "name": "按摩椅", + "id": 114 + }, + { + "parentId": 104, + "key": "Shared washing machine", + "name": "共享洗衣机", + "id": 115 + }, + { + "parentId": 54, + "key": "AromaDiffuser", + "name": "香薰机", + "id": 116 + }, + { + "parentId": 0, + "key": "SmartTemplate", + "name": "智能模板", + "id": 117 + }, + { + "parentId": 117, + "key": "WifiTemplate", + "name": "Wifi功能模板", + "id": 118 + }, + { + "parentId": 117, + "key": "ZigbeeTemplate", + "name": "Zigbee功能模板", + "id": 119 + }, + { + "parentId": 42, + "key": "SmartDoor", + "name": "智能门锁", + "id": 120 + }, + { + "parentId": 104, + "key": "Charging pile", + "name": "充电桩", + "id": 121 + }, + { + "parentId": 42, + "key": "IlluminationSensor", + "name": "光照度传感器", + "id": 122 + }, + { + "parentId": 58, + "key": "SceneSwitch", + "name": "场景开关", + "id": 123 + }, + { + "parentId": 42, + "key": "Siren", + "name": "声光报警器", + "id": 124 + }, + { + "parentId": 66, + "key": "DishWasher", + "name": "洗碗机", + "id": 125 + }, + { + "parentId": 66, + "key": "BreakingMachine", + "name": "破壁机", + "id": 130 + }, + { + "parentId": 66, + "key": "MicrowaveOven", + "name": "微波炉", + "id": 131 + }, + { + "parentId": 66, + "key": "RiceCooker", + "name": "电饭煲", + "id": 132 + }, + { + "parentId": 16, + "key": "PHSensor", + "name": "酸碱度监测", + "id": 133 + }, + { + "parentId": 16, + "key": "DOSensor", + "name": "溶解氧监测", + "id": 134 + }, + { + "parentId": 17, + "key": "GasLeakAlarm", + "name": "燃气泄漏报警器", + "id": 135 + }, + { + "parentId": 17, + "key": "FireButton", + "name": "消防手报", + "id": 136 + }, + { + "parentId": 17, + "key": "WaterPressureSensor", + "name": "水压传感器", + "id": 137 + }, + { + "parentId": 2, + "key": "WaterloggingSensor", + "name": "水浸检测", + "id": 138 + }, + { + "parentId": 2, + "key": "ManholesCoverShiftDetection", + "name": "井盖移位检测", + "id": 139 + }, + { + "parentId": 2, + "key": "GarbageOverflowingDetection", + "name": "垃圾满溢检测", + "id": 140 + }, + { + "parentId": 2, + "key": "GeomagneticSensor", + "name": "地磁检测器", + "id": 141 + }, + { + "parentId": 2, + "key": "InfraredDetectors", + "name": "红外体征探测器", + "id": 142 + }, + { + "parentId": 2, + "key": "ActiveInfraredIntrusionDetectors", + "name": "红外对射探测器", + "id": 143 + }, + { + "parentId": 2, + "key": "UnmannedAerialVehicle", + "name": "无人机", + "id": 144 + }, + { + "parentId": 2, + "key": "DoorSensor", + "name": "门磁", + "id": 145 + }, + { + "parentId": 66, + "key": "MilkModulator", + "name": "调奶器", + "id": 146 + }, + { + "parentId": 68, + "key": "Threadmill", + "name": "跑步机", + "id": 147 + }, + { + "parentId": 42, + "key": "Camera", + "name": "摄像头", + "id": 148 + }, + { + "parentId": 42, + "key": "Doorbell", + "name": "门铃", + "id": 150 + }, + { + "parentId": 42, + "key": "DoorViewer", + "name": "猫眼", + "id": 151 + }, + { + "parentId": 45, + "key": "electricmeter", + "name": "电力仪表", + "id": 152 + }, + { + "parentId": 2, + "key": "IntelligentBroadcast", + "name": "智能广播", + "id": 153 + }, + { + "parentId": 66, + "key": "SoyMilkMaker", + "name": "豆浆机", + "id": 154 + }, + { + "parentId": 68, + "key": "ECGCard", + "name": "心电卡", + "id": 155 + }, + { + "parentId": 68, + "key": "Thermometer", + "name": "体温计", + "id": 156 + }, + { + "parentId": 78, + "key": "PetFeeder", + "name": "宠物喂食机", + "id": 157 + }, + { + "parentId": 54, + "key": "RobotCleaner", + "name": "扫地机器人", + "id": 158 + }, + { + "parentId": 2, + "key": "BroadcastController", + "name": "广播主机", + "id": 159 + }, + { + "parentId": 2, + "key": "Flowmeter", + "name": "流量计", + "id": 160 + }, + { + "parentId": 2, + "key": "RemoteTerminalUnit", + "name": "远程监测终端", + "id": 161 + }, + { + "parentId": 2, + "key": "SignalCollector", + "name": "游乐设备信号采集器", + "id": 162 + }, + { + "parentId": 68, + "key": "BodyFatScale", + "name": "体脂秤", + "id": 163 + }, + { + "parentId": 2, + "key": "ConversionGateway", + "name": "通用网关", + "id": 164 + }, + { + "parentId": 2, + "key": "FaceRecognition", + "name": "人脸识别门禁", + "id": 165 + }, + { + "parentId": 42, + "key": "WarningGW", + "name": "报警网关", + "id": 166 + }, + { + "parentId": 42, + "key": "CircuitBreaker", + "name": "断路器", + "id": 167 + }, + { + "parentId": 66, + "key": "PressureCooker", + "name": "电压力锅", + "id": 168 + }, + { + "parentId": 68, + "key": "Washer", + "name": "洗衣机", + "id": 169 + }, + { + "parentId": 68, + "key": "IntelligentMassageChair", + "name": "智能按摩椅", + "id": 170 + }, + { + "parentId": 42, + "key": "FaceRecognitionCapabilityModel", + "name": "人脸识别能力模型", + "id": 171 + }, + { + "parentId": 66, + "key": "ElectricKettle", + "name": "电水壶", + "id": 172 + }, + { + "parentId": 106, + "key": "NAS", + "name": "网络存储器", + "id": 173 + }, + { + "parentId": 2, + "key": "ElevatorCollectingBox", + "name": "电梯集采盒", + "id": 174 + }, + { + "parentId": 66, + "key": "HealthPreservingPot", + "name": "养生壶", + "id": 175 + }, + { + "parentId": 66, + "key": "BreadMachine", + "name": "面包机", + "id": 176 + }, + { + "parentId": 2, + "key": "ArcExtinguishing", + "name": "电弧灭弧", + "id": 177 + }, + { + "parentId": 2, + "key": "ElevatorStatusSensor", + "name": "电梯门体状态探测传感器", + "id": 178 + }, + { + "parentId": 2, + "key": "ElevatorPositionSensor", + "name": "电梯平层位置探测传感器", + "id": 179 + }, + { + "parentId": 2, + "key": "ElevatorBodySensor", + "name": "电梯人体探测传感器", + "id": 180 + }, + { + "parentId": 2, + "key": "ElevatorAccelerationSensor", + "name": "电梯加速度探测传感器", + "id": 181 + }, + { + "parentId": 2, + "key": "TiltSensor", + "name": "倾角传感器", + "id": 182 + }, + { + "parentId": 19, + "key": "AttendanceMachine", + "name": "考勤机", + "id": 183 + }, + { + "parentId": 42, + "key": "OutdoorStation", + "name": "门口机", + "id": 184 + }, + { + "parentId": 2, + "key": "Counter", + "name": "无人货柜", + "id": 185 + }, + { + "parentId": 42, + "key": "AlarmSwitch", + "name": "报警开关", + "id": 186 + }, + { + "parentId": 16, + "key": "TemperatureHumidityDetector", + "name": "温湿度检测", + "id": 187 + }, + { + "parentId": 2, + "key": "MicrowaveDetector", + "name": "微波人体探测器", + "id": 189 + }, + { + "parentId": 0, + "key": "CustomCategory", + "name": "自定义品类", + "id": 190 + }, + { + "parentId": 58, + "key": "SmartElectricityMeter", + "name": "智能电表", + "id": 191 + }, + { + "parentId": 58, + "key": "SmartWaterMeter", + "name": "智能水表", + "id": 192 + }, + { + "parentId": 42, + "key": "PressureAlarm", + "name": "压力报警器", + "id": 193 + }, + { + "parentId": 42, + "key": "LiquidLevelAlarm", + "name": "液位传感器", + "id": 194 + }, + { + "parentId": 2, + "key": "ParkingLock", + "name": "地锁", + "id": 195 + }, + { + "parentId": 16, + "key": "WaterMonitoring", + "name": "水质检测终端", + "id": 196 + }, + { + "parentId": 66, + "key": "CapsuleCoffeeMachine", + "name": "胶囊咖啡机", + "id": 197 + }, + { + "parentId": 42, + "key": "TempHumiUnit", + "name": "温湿度采集单元", + "id": 198 + }, + { + "parentId": 42, + "key": "HazardWarningLamp", + "name": "危险报警器", + "id": 199 + }, + { + "parentId": 16, + "key": "liquidometer", + "name": "液位计", + "id": 200 + }, + { + "parentId": 2, + "key": "InternetProtocolCamera", + "name": "网络摄像机", + "id": 201 + }, + { + "parentId": 15, + "key": "GasMeter", + "name": "燃气表", + "id": 202 + }, + { + "parentId": 16, + "key": "EnvironmentalNoiseMonitor", + "name": "环境噪音监测", + "id": 203 + }, + { + "parentId": 16, + "key": "DustMonitor", + "name": "扬尘监测", + "id": 204 + }, + { + "parentId": 2, + "key": "AlarmButton", + "name": "手动求救报警", + "id": 205 + }, + { + "parentId": 63, + "key": "Television", + "name": "电视机", + "id": 206 + }, + { + "parentId": 2, + "key": "ChargingPile", + "name": "非机动车充电桩", + "id": 207 + }, + { + "parentId": 66, + "key": "InductionCooker", + "name": "电磁炉", + "id": 208 + }, + { + "parentId": 2, + "key": "Locator", + "name": "定位器", + "id": 209 + }, + { + "parentId": 2, + "key": "DisplacementMonitor", + "name": "位移监控器", + "id": 210 + }, + { + "parentId": 66, + "key": "AutomaticCooker", + "name": "烹饪机器人", + "id": 211 + }, + { + "parentId": 0, + "key": "SOCSmartLife", + "name": "智能生活SOC", + "id": 213 + }, + { + "parentId": 213, + "key": "SOCOutlet", + "name": "SoC插座", + "id": 214 + }, + { + "parentId": 214, + "key": "SingleSlotOutlet", + "name": "SoC单孔插座", + "id": 215 + }, + { + "parentId": 68, + "key": "Sphygmomanometer", + "name": "电子血压计", + "id": 216 + }, + { + "parentId": 42, + "key": "ParkingDetector", + "name": "车位检测器", + "id": 217 + }, + { + "parentId": 42, + "key": "ParkingLotBarrier", + "name": "车位锁", + "id": 218 + }, + { + "parentId": 66, + "key": "Oven", + "name": "烤箱", + "id": 219 + }, + { + "parentId": 17, + "key": "SmartFireHydrants", + "name": "智能消防栓", + "id": 220 + }, + { + "parentId": 45, + "key": "ImageCaptureDevice", + "name": "图像采集设备", + "id": 221 + }, + { + "parentId": 0, + "key": "ElectricPower", + "name": "智能电力", + "id": 222 + }, + { + "parentId": 2, + "key": "OffVoltageMonitor", + "name": "断电监控", + "id": 223 + }, + { + "parentId": 78, + "key": "ElectricMotorcycle", + "name": "电动摩托车", + "id": 224 + }, + { + "parentId": 66, + "key": "Sterilizer", + "name": "消毒柜", + "id": 225 + }, + { + "parentId": 66, + "key": "FoodDispenser", + "name": "取餐柜", + "id": 227 + }, + { + "parentId": 78, + "key": "PricingScale", + "name": "计价秤", + "id": 228 + }, + { + "parentId": 58, + "key": "Scene", + "name": "场景面板", + "id": 231 + }, + { + "parentId": 66, + "key": "EmbeddedSteamer", + "name": "嵌入式电蒸箱", + "id": 233 + }, + { + "parentId": 78, + "key": "Audio", + "name": "音箱", + "id": 234 + }, + { + "parentId": 42, + "key": "VibrationSensor", + "name": "震动传感器", + "id": 235 + }, + { + "parentId": 2, + "key": "ElectricSafetyDetector", + "name": "用电安全探测器", + "id": 236 + }, + { + "parentId": 42, + "key": "TitrantPump", + "name": "滴定泵", + "id": 237 + }, + { + "parentId": 63, + "key": "AirEnergyHeater", + "name": "空气能热水器", + "id": 238 + }, + { + "parentId": 66, + "key": "TeaBar", + "name": "茶吧机", + "id": 239 + }, + { + "parentId": 2, + "key": "CuttingMachine", + "name": "裁床", + "id": 240 + }, + { + "parentId": 0, + "key": "SmartAgriculture", + "name": "智能农业", + "id": 241 + }, + { + "parentId": 241, + "key": "Plant", + "name": "种植", + "id": 242 + }, + { + "parentId": 241, + "key": "Forestry", + "name": "林业", + "id": 243 + }, + { + "parentId": 241, + "key": "Stockbreeding", + "name": "畜牧", + "id": 244 + }, + { + "parentId": 241, + "key": "Aquatic", + "name": "水产", + "id": 245 + }, + { + "parentId": 58, + "key": "ThreePhaseMeter", + "name": "三相电表", + "id": 247 + }, + { + "parentId": 106, + "key": "HomeLinkEdgeGateway", + "name": "全屋边缘网关", + "id": 248 + }, + { + "parentId": 66, + "key": "WallHungGasBoiler", + "name": "壁挂炉", + "id": 249 + }, + { + "parentId": 58, + "key": "CeilingFanLamp", + "name": "吊扇灯", + "id": 250 + }, + { + "parentId": 241, + "key": "WindSensor", + "name": "风速传感器", + "id": 251 + }, + { + "parentId": 241, + "key": "SolubleSensor", + "name": "可溶性盐传感器", + "id": 252 + }, + { + "parentId": 241, + "key": "DioxideDetector", + "name": "二氧化碳检测器", + "id": 253 + }, + { + "parentId": 241, + "key": "FlowDetection", + "name": "营养液流速监测器", + "id": 254 + }, + { + "parentId": 241, + "key": "PHDetector", + "name": "酸碱度检测计", + "id": 255 + }, + { + "parentId": 241, + "key": "ToxicGas", + "name": "有害气体检测器", + "id": 256 + }, + { + "parentId": 241, + "key": "LightEnsor", + "name": "光照传感器", + "id": 257 + }, + { + "parentId": 0, + "key": "Building", + "name": "智慧建筑", + "id": 258 + }, + { + "parentId": 258, + "key": "FaceServer", + "name": "人脸门禁", + "id": 259 + }, + { + "parentId": 258, + "key": "ParkOverAll", + "name": "道闸一体机", + "id": 260 + }, + { + "parentId": 258, + "key": "ParkMagnetism", + "name": "地磁车位监测器", + "id": 261 + }, + { + "parentId": 258, + "key": "EnvSensor", + "name": "九合一环境传感器", + "id": 262 + }, + { + "parentId": 258, + "key": "ParkHDDetectMachine", + "name": "车位传感器", + "id": 263 + }, + { + "parentId": 241, + "key": "PigDataReader", + "name": "猪参数采集器", + "id": 264 + }, + { + "parentId": 19, + "key": "epd_table", + "name": "电子纸桌签", + "id": 265 + }, + { + "parentId": 242, + "key": "SideWindow", + "name": "侧窗", + "id": 266 + }, + { + "parentId": 242, + "key": "WaterAndFertilizerMachine", + "name": "水肥一体机", + "id": 267 + }, + { + "parentId": 242, + "key": "CarbonDioxideGeneratingDevice", + "name": "二氧化碳发生装置", + "id": 268 + }, + { + "parentId": 242, + "key": "FillLight", + "name": "补光灯", + "id": 269 + }, + { + "parentId": 242, + "key": "HotAirBlower", + "name": "热风机", + "id": 270 + }, + { + "parentId": 242, + "key": "GroundSourceHeatPump", + "name": "地源热泵", + "id": 271 + }, + { + "parentId": 242, + "key": "WetCurtain", + "name": "湿帘", + "id": 272 + }, + { + "parentId": 242, + "key": "InnerInsulationCurtain", + "name": "内保温帘幕", + "id": 273 + }, + { + "parentId": 242, + "key": "OutsideShadeCurtain", + "name": "外遮荫帘幕", + "id": 274 + }, + { + "parentId": 242, + "key": "CirculatingFan", + "name": "环流风机", + "id": 275 + }, + { + "parentId": 242, + "key": "SideFan", + "name": "侧风机", + "id": 276 + }, + { + "parentId": 242, + "key": "SkyWindow", + "name": "天窗", + "id": 277 + }, + { + "parentId": 87, + "key": "VisionAccessNode", + "name": "摄像头边缘节点", + "id": 278 + }, + { + "parentId": 258, + "key": "ParkWNC", + "name": "超声车位检测器", + "id": 279 + }, + { + "parentId": 258, + "key": "FeatureCamera", + "name": "特征摄像头", + "id": 280 + }, + { + "parentId": 258, + "key": "BrushFace", + "name": "扫脸娃娃机", + "id": 281 + }, + { + "parentId": 258, + "key": "PeopleFlow", + "name": "客流量传感器", + "id": 282 + }, + { + "parentId": 258, + "key": "FaceIdentification", + "name": "客群识别设备", + "id": 283 + }, + { + "parentId": 258, + "key": "TicketMachine", + "name": "停车小票机设备", + "id": 284 + }, + { + "parentId": 241, + "key": "SmartHives", + "name": "智能蜂箱", + "id": 285 + }, + { + "parentId": 78, + "key": "EpdTable", + "name": "电子标签", + "id": 286 + }, + { + "parentId": 78, + "key": "LocalControlCenter", + "name": "中控屏", + "id": 287 + }, + { + "parentId": 78, + "key": "IRRemoteController", + "name": "红外遥控器", + "id": 288 + }, + { + "parentId": 241, + "key": "Register", + "name": "寄存器", + "id": 289 + }, + { + "parentId": 258, + "key": "QuickAccessDoor", + "name": "速通门", + "id": 290 + }, + { + "parentId": 258, + "key": "FingerPrintDoor", + "name": "指纹门禁", + "id": 291 + }, + { + "parentId": 258, + "key": "ParkLed", + "name": "余位显示屏", + "id": 292 + }, + { + "parentId": 258, + "key": "GuideScreen", + "name": "导购屏", + "id": 293 + }, + { + "parentId": 258, + "key": "GovernmentLed", + "name": "路政设备", + "id": 294 + }, + { + "parentId": 78, + "key": "Watch", + "name": "手表", + "id": 295 + }, + { + "parentId": 16, + "key": "PreciseTimeSpaceCamera", + "name": "精准时空摄像头", + "id": 296 + }, + { + "parentId": 241, + "key": "SmartPatrol", + "name": "智能巡护", + "id": 297 + }, + { + "parentId": 241, + "key": "EnvironmentMonitoring", + "name": "环境监测", + "id": 298 + }, + { + "parentId": 42, + "key": "Cateyecamera", + "name": "猫眼摄像头", + "id": 299 + }, + { + "parentId": 0, + "key": "campus", + "name": "智能园区", + "id": 301 + }, + { + "parentId": 258, + "key": "N4DeviceType", + "name": "N4设备", + "id": 302 + }, + { + "parentId": 258, + "key": "Car_Detector_Cam", + "name": "车牌抓拍", + "id": 304 + }, + { + "parentId": 301, + "key": "loraLight", + "name": "lora单灯", + "id": 305 + }, + { + "parentId": 301, + "key": "Gatewaycampus", + "name": "网关-园区", + "id": 306 + }, + { + "parentId": 301, + "key": "Soilsensor", + "name": "土壤传感器", + "id": 307 + }, + { + "parentId": 258, + "key": "capture", + "name": "车牌抓拍-ib", + "id": 308 + }, + { + "parentId": 301, + "key": "Curtainswitch", + "name": "单路窗帘开关", + "id": 309 + }, + { + "parentId": 301, + "key": "Sceneswitch2", + "name": "场景面板开关", + "id": 310 + }, + { + "parentId": 18, + "key": "irrigation", + "name": "灌溉系统", + "id": 311 + }, + { + "parentId": 78, + "key": "Teatable", + "name": "茶台", + "id": 312 + }, + { + "parentId": 78, + "key": "IntelligentCurtain", + "name": "智能窗帘", + "id": 313 + }, + { + "parentId": 301, + "key": "MeteorologicalStation", + "name": "气象站监控仪", + "id": 314 + }, + { + "parentId": 301, + "key": "CurrentTemperature", + "name": "室内温度传感器", + "id": 315 + }, + { + "parentId": 301, + "key": "PowerSwitch2", + "name": "入墙开关2", + "id": 316 + }, + { + "parentId": 106, + "key": "VOCSensor", + "name": "VOC感应器", + "id": 319 + }, + { + "parentId": 66, + "key": "Ice_machine", + "name": "制冰机", + "id": 320 + }, + { + "parentId": 58, + "key": "Metering_socket", + "name": "带计量功能插座", + "id": 322 + }, + { + "parentId": 58, + "key": "Lamp", + "name": "灯控开关", + "id": 323 + }, + { + "parentId": 42, + "key": "Curtain_motor", + "name": "窗帘电机", + "id": 324 + }, + { + "parentId": 58, + "key": "Dimming_panel", + "name": "家居调光面板", + "id": 325 + }, + { + "parentId": 54, + "key": "air_sensor", + "name": "环境检测盒子", + "id": 326 + }, + { + "parentId": 68, + "key": "Smart_Neck_Massage", + "name": "智能颈部按摩仪", + "id": 327 + }, + { + "parentId": 68, + "key": "SmartBoxingTarget", + "name": "智能拳靶", + "id": 328 + }, + { + "parentId": 68, + "key": "SmartCleanFace", + "name": "智能洁面仪", + "id": 329 + }, + { + "parentId": 106, + "key": "water_logging", + "name": "水浸传感器", + "id": 330 + }, + { + "parentId": 106, + "key": "Smoke_sensor", + "name": "烟感传感器", + "id": 331 + }, + { + "parentId": 42, + "key": "emergency_button", + "name": "紧急按钮", + "id": 332 + }, + { + "parentId": 45, + "key": "Gas meter manufacturing", + "name": "气表制造", + "id": 335 + }, + { + "parentId": 335, + "key": "AirCompressorMachine", + "name": "空压机", + "id": 336 + }, + { + "parentId": 335, + "key": "Spraying", + "name": "喷涂处理", + "id": 337 + }, + { + "parentId": 335, + "key": "GlueSprayingMachine", + "name": "涂胶机", + "id": 338 + }, + { + "parentId": 335, + "key": "PunchingMachine", + "name": "冲压机", + "id": 339 + }, + { + "parentId": 301, + "key": "Temperatureandhumiditysensor", + "name": "室内温湿度监测设备", + "id": 340 + }, + { + "parentId": 301, + "key": "Airconditionerthermostat", + "name": "空调温控器", + "id": 341 + }, + { + "parentId": 301, + "key": "MAXHUB", + "name": "会议平板", + "id": 342 + }, + { + "parentId": 2, + "key": "WiFiProbeCollector", + "name": "WiFi探针采集器", + "id": 343 + }, + { + "parentId": 45, + "key": "Platinum_Temperature", + "name": "铂电阻温度传感器", + "id": 344 + }, + { + "parentId": 301, + "key": "CoSee", + "name": "大屏投放终端", + "id": 345 + }, + { + "parentId": 16, + "key": "Seeper", + "name": "易涝点监测设备", + "id": 347 + }, + { + "parentId": 16, + "key": "ManholeLevel", + "name": "窨井液位监测设备", + "id": 348 + }, + { + "parentId": 16, + "key": "RainGauge", + "name": "雨量计", + "id": 349 + }, + { + "parentId": 16, + "key": "FlowRate", + "name": "流速液位监测设备", + "id": 350 + }, + { + "parentId": 87, + "key": "AlgorithmManagerPlatform", + "name": "视频内容分析", + "id": 351 + }, + { + "parentId": 2, + "key": "Collision data collection", + "name": "行车碰撞数据采集", + "id": 352 + }, + { + "parentId": 2, + "key": "Drivingbehavior", + "name": "驾驶行为数据采集", + "id": 353 + }, + { + "parentId": 2, + "key": "Logisticsmonitoring", + "name": "仓储运输环境检测设备", + "id": 354 + }, + { + "parentId": 78, + "key": "FishTank", + "name": "鱼缸", + "id": 355 + }, + { + "parentId": 106, + "key": "Currentdetectingsensor", + "name": "单向电流检测传感器", + "id": 356 + }, + { + "parentId": 106, + "key": "Tracker", + "name": "定位终端", + "id": 357 + }, + { + "parentId": 258, + "key": "IB_Lock ", + "name": "锁", + "id": 358 + }, + { + "parentId": 301, + "key": "ParkChannel", + "name": "社区车行停车通道", + "id": 359 + }, + { + "parentId": 301, + "key": "ParkArea", + "name": "社区车行停车区域", + "id": 360 + }, + { + "parentId": 301, + "key": "SmartWaterFlowMeter", + "name": "智能水流量计", + "id": 361 + }, + { + "parentId": 301, + "key": "SmartGasFlowMeter", + "name": "智能燃气流量计", + "id": 362 + }, + { + "parentId": 301, + "key": "MultifunctionElectricityMeter", + "name": "多功能电表", + "id": 363 + }, + { + "parentId": 301, + "key": "QrCodeAccess", + "name": "二维码门禁", + "id": 364 + }, + { + "parentId": 68, + "key": "TowelRack", + "name": "毛巾架", + "id": 365 + }, + { + "parentId": 66, + "key": "Airfryer", + "name": "空气炸锅", + "id": 366 + }, + { + "parentId": 66, + "key": "WaterSoftener", + "name": "软水机", + "id": 367 + }, + { + "parentId": 258, + "key": "MeterElec_Dlt645", + "name": "DLT645电表", + "id": 406 + }, + { + "parentId": 66, + "key": "NoodleMaker", + "name": "面条机", + "id": 407 + }, + { + "parentId": 301, + "key": "PasswordAccess", + "name": "密码门禁", + "id": 408 + }, + { + "parentId": 301, + "key": "CardAccess", + "name": "刷卡门禁", + "id": 409 + }, + { + "parentId": 258, + "key": "SmartDoorIntercoms", + "name": "门禁对讲机", + "id": 413 + }, + { + "parentId": 78, + "key": "SensorSignalCollector", + "name": "传感器信号采集器", + "id": 415 + }, + { + "parentId": 78, + "key": "HVACExtController", + "name": "HVAC外接控制器", + "id": 416 + }, + { + "parentId": 78, + "key": "BackgroundMusicController", + "name": "背景音乐控制器", + "id": 418 + }, + { + "parentId": 301, + "key": "ParkingSpace", + "name": "停车车位", + "id": 431 + }, + { + "parentId": 301, + "key": "ParkingArea", + "name": "停车区域", + "id": 432 + }, + { + "parentId": 301, + "key": "ParkingBarrier", + "name": "停车道闸", + "id": 433 + }, + { + "parentId": 301, + "key": "ParkingLot", + "name": "停车场", + "id": 434 + }, + { + "parentId": 78, + "key": "AutoDoor", + "name": "自动门", + "id": 437 + }, + { + "parentId": 258, + "key": "EFence", + "name": "电子围栏", + "id": 438 + }, + { + "parentId": 78, + "key": "MosquitoLamp", + "name": "灭蚊器", + "id": 563 + }, + { + "parentId": 78, + "key": "LawnMower", + "name": "割草机", + "id": 675 + }, + { + "parentId": 301, + "key": "HIKVISIONEdgeServer", + "name": "海康边缘服务器", + "id": 678 + }, + { + "parentId": 78, + "key": "Relay", + "name": "继电器", + "id": 2143 + }, + { + "parentId": 66, + "key": "IntegratedStove", + "name": "集成灶", + "id": 2184 + }, + { + "parentId": 66, + "key": "IceCreamMaker", + "name": "冰激凌机", + "id": 2187 + }, + { + "parentId": 54, + "key": "VoiceTemperatureControlPanel", + "name": "语音温控面板", + "id": 2188 + }, + { + "parentId": 68, + "key": "SmartPillow", + "name": "智能枕", + "id": 2189 + }, + { + "parentId": 17, + "key": "FireDoor", + "name": "防火门", + "id": 2192 + }, + { + "parentId": 78, + "key": "Dryer", + "name": "干衣机", + "id": 2193 + }, + { + "parentId": 17, + "key": "EmergencyLight", + "name": "应急照明灯", + "id": 2194 + }, + { + "parentId": 17, + "key": "FireWaterCannon", + "name": "智能消防水炮", + "id": 2195 + }, + { + "parentId": 17, + "key": "FireCannon", + "name": "智能消防炮", + "id": 2196 + }, + { + "parentId": 2, + "key": "SmartDustbin", + "name": "智能垃圾桶", + "id": 2197 + }, + { + "parentId": 17, + "key": "PressureBlower", + "name": "正压送风机", + "id": 2198 + }, + { + "parentId": 17, + "key": "FlowIndicator", + "name": "水流指示器", + "id": 2199 + }, + { + "parentId": 17, + "key": "ElectricValve", + "name": "电动阀", + "id": 2200 + }, + { + "parentId": 17, + "key": "ExhaustWindow", + "name": "电动排烟窗", + "id": 2201 + }, + { + "parentId": 17, + "key": "EmerBroadcasting", + "name": "应急广播", + "id": 2202 + }, + { + "parentId": 17, + "key": "FireValve", + "name": "防火阀", + "id": 2203 + }, + { + "parentId": 258, + "key": "SmartElevator", + "name": "智能电梯", + "id": 2215 + }, + { + "parentId": 78, + "key": "SmartGasMeter", + "name": "智能燃气表", + "id": 2216 + }, + { + "parentId": 258, + "key": "ElectricVehicleChargingStation", + "name": "电动汽车充电站", + "id": 2221 + }, + { + "parentId": 301, + "key": "ElevatorController", + "name": "梯控", + "id": 2224 + }, + { + "parentId": 66, + "key": "FoodDehydrator", + "name": "食物烘干机", + "id": 2226 + }, + { + "parentId": 68, + "key": "MoxibustionApparatus", + "name": "艾灸仪", + "id": 2227 + }, + { + "parentId": 258, + "key": "VREquipment", + "name": "智能VR类设备", + "id": 2228 + }, + { + "parentId": 78, + "key": "IntelligentLitterBox", + "name": "智能猫砂盆", + "id": 2229 + }, + { + "parentId": 42, + "key": "NVR", + "name": "网络硬盘录像机", + "id": 2242 + }, + { + "parentId": 78, + "key": "SmartWasteSortingDustBin", + "name": "智能分类垃圾桶", + "id": 2243 + }, + { + "parentId": 66, + "key": "GrainMill", + "name": "谷物碾磨机", + "id": 2254 + }, + { + "parentId": 78, + "key": "ShowerHead", + "name": "花洒", + "id": 2261 + }, + { + "parentId": 301, + "key": "QrCodeAccessControl", + "name": "二维码门禁机", + "id": 2262 + }, + { + "parentId": 2, + "key": "ActiveVehMaintenance", + "name": "车辆主动维护", + "id": 2263 + }, + { + "parentId": 2, + "key": "SmartTox", + "name": "智能车机", + "id": 2264 + }, + { + "parentId": 2, + "key": "SmartRearviewMirror", + "name": "智能后视镜", + "id": 2265 + }, + { + "parentId": 301, + "key": "VideoIntercomDoor", + "name": "可视对讲机门口机", + "id": 2268 + }, + { + "parentId": 301, + "key": "BluetoothAccess", + "name": "蓝牙门禁", + "id": 2269 + }, + { + "parentId": 78, + "key": "Projector", + "name": "投影仪", + "id": 2291 + }, + { + "parentId": 87, + "key": "FaceRecognizeDevice", + "name": "人脸识别机", + "id": 2294 + }, + { + "parentId": 301, + "key": "AdvertTerminal", + "name": "广告屏", + "id": 2296 + }, + { + "parentId": 241, + "key": "FarmRecorder", + "name": "农田记录仪", + "id": 2298 + }, + { + "parentId": 78, + "key": "Faucet", + "name": "龙头", + "id": 2306 + } +] \ No newline at end of file diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/device/DeviceAlarmRule.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/device/DeviceAlarmRule.java index 92dc1fe1..ff0d1b54 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/device/DeviceAlarmRule.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/device/DeviceAlarmRule.java @@ -73,6 +73,11 @@ public class DeviceAlarmRule implements Serializable { */ private List actions; + /** + * 防抖限制 + */ + private ShakeLimit shakeLimit; + public void validate() { if (org.apache.commons.collections.CollectionUtils.isEmpty(getTriggers())) { @@ -286,6 +291,28 @@ public class DeviceAlarmRule implements Serializable { } + /** + * 抖动限制 + * https://github.com/jetlinks/jetlinks-community/issues/8 + * + * @since 1.3 + */ + @Getter + @Setter + public static class ShakeLimit implements Serializable { + private boolean enabled; + + //时间限制,单位时间内发生多次告警时,只算一次。单位:秒 + private int time; + + //触发阈值,单位时间内发生n次告警,只算一次。 + private int threshold; + + //当发生第一次告警时就触发,为false时表示最后一次才触发(告警有延迟,但是可以统计出次数) + private boolean alarmFirst; + + } + @AllArgsConstructor @Getter public enum Operator { diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/device/DeviceAlarmRuleNode.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/device/DeviceAlarmTaskExecutorProvider.java similarity index 60% rename from jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/device/DeviceAlarmRuleNode.java rename to jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/device/DeviceAlarmTaskExecutorProvider.java index 08bbb743..deab2c6e 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/device/DeviceAlarmRuleNode.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/device/DeviceAlarmTaskExecutorProvider.java @@ -1,72 +1,58 @@ package org.jetlinks.community.rule.engine.device; import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; -import org.jetlinks.core.message.DeviceMessage; +import org.hswebframework.web.bean.FastBeanCopier; +import org.jetlinks.community.ValueObject; import org.jetlinks.community.gateway.DeviceMessageUtils; import org.jetlinks.community.gateway.MessageGateway; import org.jetlinks.community.gateway.Subscription; +import org.jetlinks.core.message.DeviceMessage; import org.jetlinks.reactor.ql.ReactorQL; import org.jetlinks.reactor.ql.ReactorQLContext; import org.jetlinks.reactor.ql.ReactorQLRecord; +import org.jetlinks.rule.engine.api.RuleConstants; import org.jetlinks.rule.engine.api.RuleData; -import org.jetlinks.rule.engine.api.events.RuleEvent; -import org.jetlinks.rule.engine.api.executor.ExecutionContext; -import org.jetlinks.rule.engine.api.model.NodeType; -import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy; -import org.jetlinks.rule.engine.executor.node.RuleNodeConfig; +import org.jetlinks.rule.engine.api.task.ExecutionContext; +import org.jetlinks.rule.engine.api.task.Task; +import org.jetlinks.rule.engine.api.task.TaskExecutor; +import org.jetlinks.rule.engine.api.task.TaskExecutorProvider; +import org.jetlinks.rule.engine.defaults.AbstractTaskExecutor; import org.reactivestreams.Publisher; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; +import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; +import java.time.Duration; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; -@Slf4j(topic = "system.rule.engine.device.alarm") -@Component +@Slf4j @AllArgsConstructor -public class DeviceAlarmRuleNode extends CommonExecutableRuleNodeFactoryStrategy { +@Component +public class DeviceAlarmTaskExecutorProvider implements TaskExecutorProvider { private final MessageGateway messageGateway; @Override - public Function> createExecutor(ExecutionContext context,DeviceAlarmRuleNode.Config config) { - - return Mono::just; - } - - @Override - protected void onStarted(ExecutionContext context, DeviceAlarmRuleNode.Config config) { - context.onStop( - config.doSubscribe(messageGateway) - .flatMap(result -> { - RuleData data = RuleData.create(result); - //输出到下一节点 - return context.getOutput() - .write(Mono.just(data)) - .then(context.fireEvent(RuleEvent.NODE_EXECUTE_DONE, data)); - }) - .onErrorResume(err -> context.onError(RuleData.create(err.getMessage()), err)) - .subscribe()::dispose - ); - } - - @Override - public String getSupportType() { + public String getExecutor() { return "device_alarm"; } + @Override + public Mono createTask(ExecutionContext context) { + return Mono.just(new DeviceAlarmTaskExecutor(context)); + } - @Getter - @Setter - public static class Config implements RuleNodeConfig { - static List default_columns = Arrays.asList( + class DeviceAlarmTaskExecutor extends AbstractTaskExecutor { + + List default_columns = Arrays.asList( "timestamp", "deviceId", "this.header.deviceName deviceName" ); @@ -74,19 +60,63 @@ public class DeviceAlarmRuleNode extends CommonExecutableRuleNodeFactoryStrategy private ReactorQL ql; + public DeviceAlarmTaskExecutor(ExecutionContext context) { + super(context); + rule = createRule(); + ql = createQL(rule); + } + + @Override + public String getName() { + return "设备告警"; + } + + @Override + protected Disposable doStart() { + rule.validate(); + return doSubscribe(messageGateway) + .filter(ignore -> state == Task.State.running) + .flatMap(result -> { + RuleData data = RuleData.create(result); + //输出到下一节点 + return context + .getOutput() + .write(Mono.just(data)) + .then(context.fireEvent(RuleConstants.Event.result, data)); + }) + .onErrorResume(err -> context.onError(err, null)) + .subscribe(); + } + + @Override + public void reload() { + rule = createRule(); + ql = createQL(rule); + if (disposable != null) { + disposable.dispose(); + } + doStart(); + } + + private DeviceAlarmRule createRule() { + DeviceAlarmRule rule = ValueObject.of(context.getJob().getConfiguration()) + .get("rule") + .map(val -> FastBeanCopier.copy(val, new DeviceAlarmRule())).orElseThrow(() -> new IllegalArgumentException("告警配置错误")); + rule.validate(); + return rule; + } + @Override public void validate() { - if (CollectionUtils.isEmpty(rule.getTriggers())) { - throw new IllegalArgumentException("预警条件不能为空"); - } + DeviceAlarmRule rule = createRule(); try { - ql = createQL(); + createQL(rule); } catch (Exception e) { throw new IllegalArgumentException("配置错误:" + e.getMessage(), e); } } - private ReactorQL createQL() { + private ReactorQL createQL(DeviceAlarmRule rule) { List columns = new ArrayList<>(default_columns); List wheres = new ArrayList<>(); @@ -116,13 +146,21 @@ public class DeviceAlarmRuleNode extends CommonExecutableRuleNodeFactoryStrategy continue; } String alias = StringUtils.hasText(property.getAlias()) ? property.getAlias() : property.getProperty(); - newColumns.add("this['" + property.getProperty() + "'] \"" + alias + "\""); + // 'message',func(),this[name] + if ((property.getProperty().startsWith("'") && property.getProperty().endsWith("'")) + || + property.getProperty().contains("(") || property.getProperty().contains("[")) { + newColumns.add(property.getProperty() + "\"" + alias + "\""); + } else { + newColumns.add("this['" + property.getProperty() + "'] \"" + alias + "\""); + } } if (newColumns.size() > 3) { sql = "select \n\t" + String.join("\n\t,", newColumns) + "\n from (\n\t" + sql + "\n) t"; } } log.debug("create device alarm sql : \n{}", sql); + return ReactorQL.builder().sql(sql).build(); } @@ -157,9 +195,41 @@ public class DeviceAlarmRuleNode extends CommonExecutableRuleNodeFactoryStrategy ); binds.forEach(context::bind); - return (ql == null ? ql = createQL() : ql) + + Flux> resultFlux = (ql == null ? ql = createQL(rule) : ql) .start(context) - .map(ReactorQLRecord::asMap) + .map(ReactorQLRecord::asMap); + + DeviceAlarmRule.ShakeLimit shakeLimit; + if ((shakeLimit = rule.getShakeLimit()) != null + && shakeLimit.isEnabled() + && shakeLimit.getTime() > 0) { + int thresholdNumber = shakeLimit.getThreshold(); + //打开时间窗口 + Flux>> window = resultFlux.window(Duration.ofSeconds(shakeLimit.getTime())); + + Function>>, Publisher>>> mapper = + shakeLimit.isAlarmFirst() + ? + group -> group + .takeUntil(tp -> tp.getT1() >= thresholdNumber) + .take(1) + .singleOrEmpty() + : + group -> group.takeLast(1).singleOrEmpty(); + + resultFlux = window + .flatMap(group -> group + .index((index, data) -> Tuples.of(index + 1, data)) + .transform(mapper) + .filter(tp -> tp.getT1() >= thresholdNumber) //超过阈值告警 + .map(tp2 -> { + tp2.getT2().put("totalAlarms", tp2.getT1()); + return tp2.getT2(); + })); + } + + return resultFlux .flatMap(map -> { map.put("productId", rule.getProductId()); map.put("alarmId", rule.getId()); @@ -192,15 +262,5 @@ public class DeviceAlarmRuleNode extends CommonExecutableRuleNodeFactoryStrategy .then(Mono.just(map)); }); } - - @Override - public NodeType getNodeType() { - return NodeType.MAP; - } - - @Override - public void setNodeType(NodeType nodeType) { - - } } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleInstanceEntity.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleInstanceEntity.java index 3baa2970..cccf116e 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleInstanceEntity.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleInstanceEntity.java @@ -7,7 +7,6 @@ import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec; import org.hswebframework.web.api.crud.entity.GenericEntity; import org.hswebframework.web.api.crud.entity.RecordCreationEntity; import org.jetlinks.community.rule.engine.enums.RuleInstanceState; -import org.jetlinks.rule.engine.api.Rule; import org.jetlinks.rule.engine.api.model.RuleEngineModelParser; import org.jetlinks.rule.engine.api.model.RuleModel; import org.springframework.util.StringUtils; @@ -63,14 +62,11 @@ public class RuleInstanceEntity extends GenericEntity implements RecordC private String instanceDetailJson; - public Rule toRule(RuleEngineModelParser parser) { + public RuleModel toRule(RuleEngineModelParser parser) { RuleModel model = parser.parse(modelType, modelMeta); model.setId(StringUtils.hasText(modelId)?modelId:getId()); model.setName(name); - Rule rule = new Rule(); - rule.setModel(model); - rule.setVersion(modelVersion); - rule.setId(getId()); - return rule; + + return model; } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/model/Action.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/model/Action.java index af3d73bd..e4fb9604 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/model/Action.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/model/Action.java @@ -2,9 +2,7 @@ package org.jetlinks.community.rule.engine.model; import lombok.Getter; import lombok.Setter; -import org.jetlinks.rule.engine.api.executor.RuleNodeConfiguration; import org.jetlinks.rule.engine.api.model.RuleNodeModel; -import org.jetlinks.rule.engine.executor.ExecutableRuleNodeFactoryStrategy; import java.io.Serializable; import java.util.Map; @@ -18,7 +16,6 @@ public class Action implements Serializable { * 执行器 * * @see RuleNodeModel#getExecutor() - * @see ExecutableRuleNodeFactoryStrategy#getSupportType() */ private String executor; @@ -26,7 +23,6 @@ public class Action implements Serializable { * 执行器配置 * * @see RuleNodeModel#getConfiguration() - * @see RuleNodeConfiguration */ private Map configuration; } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/model/DeviceAlarmModelParser.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/model/DeviceAlarmModelParser.java index e4dd77ec..f8b927e5 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/model/DeviceAlarmModelParser.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/model/DeviceAlarmModelParser.java @@ -3,11 +3,10 @@ package org.jetlinks.community.rule.engine.model; import com.alibaba.fastjson.JSON; import org.apache.commons.collections.CollectionUtils; import org.hswebframework.web.bean.FastBeanCopier; +import org.jetlinks.community.rule.engine.executor.DeviceMessageSendTaskExecutorProvider; import org.jetlinks.core.message.DeviceMessage; import org.jetlinks.community.rule.engine.device.DeviceAlarmRule; import org.jetlinks.community.rule.engine.entity.DeviceAlarmEntity; -import org.jetlinks.community.rule.engine.nodes.DeviceMessageSendNode; -import org.jetlinks.rule.engine.api.cluster.RunMode; import org.jetlinks.rule.engine.api.model.RuleLink; import org.jetlinks.rule.engine.api.model.RuleModel; import org.jetlinks.rule.engine.api.model.RuleNodeModel; @@ -37,7 +36,6 @@ public class DeviceAlarmModelParser implements RuleModelParserStrategy { RuleModel model = new RuleModel(); model.setId("device_alarm:".concat(rule.getId())); model.setName(rule.getName()); - model.setRunMode(RunMode.CLUSTER); DeviceAlarmRule alarmRule = rule.getAlarmRule(); alarmRule.validate(); @@ -59,7 +57,7 @@ public class DeviceAlarmModelParser implements RuleModelParserStrategy { timer.setExecutor("timer"); timer.setConfiguration(Collections.singletonMap("cron", timerTrigger.getCron())); - DeviceMessageSendNode.Config senderConfig = new DeviceMessageSendNode.Config(); + DeviceMessageSendTaskExecutorProvider.Config senderConfig = new DeviceMessageSendTaskExecutorProvider.Config(); senderConfig.setAsync(true); senderConfig.setDeviceId(alarmRule.getDeviceId()); senderConfig.setProductId(alarmRule.getProductId()); diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/model/SqlRuleModelParser.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/model/SqlRuleModelParser.java index c90f64ba..f1b99045 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/model/SqlRuleModelParser.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/model/SqlRuleModelParser.java @@ -3,7 +3,7 @@ package org.jetlinks.community.rule.engine.model; import com.alibaba.fastjson.JSON; import org.jetlinks.community.rule.engine.enums.SqlRuleType; import org.jetlinks.community.rule.engine.ql.SqlRule; -import org.jetlinks.rule.engine.api.events.RuleEvent; +import org.jetlinks.rule.engine.api.RuleConstants; import org.jetlinks.rule.engine.api.model.RuleLink; import org.jetlinks.rule.engine.api.model.RuleModel; import org.jetlinks.rule.engine.api.model.RuleNodeModel; @@ -59,7 +59,7 @@ public class SqlRuleModelParser implements RuleModelParserStrategy { link.setId(action.getId().concat(":").concat(action.getId())); link.setName("错误处理:" + index); link.setSource(sqlNode); - link.setType(RuleEvent.NODE_EXECUTE_FAIL); + link.setType(RuleConstants.Event.error); link.setTarget(action); errorHandler.add(link); model.getNodes().add(action); diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/RuleEngineDebugService.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/RuleEngineDebugService.java deleted file mode 100644 index 8d2dcee7..00000000 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/RuleEngineDebugService.java +++ /dev/null @@ -1,361 +0,0 @@ -package org.jetlinks.community.rule.engine.service; - -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.serializer.SerializerFeature; -import lombok.Getter; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.hswebframework.utils.StringUtils; -import org.hswebframework.web.exception.NotFoundException; -import org.hswebframework.web.id.IDGenerator; -import org.jetlinks.core.cluster.ClusterManager; -import org.jetlinks.core.cluster.ClusterQueue; -import org.jetlinks.core.cluster.ClusterTopic; -import org.jetlinks.rule.engine.api.*; -import org.jetlinks.rule.engine.api.executor.*; -import org.jetlinks.rule.engine.api.model.Condition; -import org.jetlinks.rule.engine.api.model.NodeType; -import org.jetlinks.rule.engine.api.model.RuleEngineModelParser; -import org.jetlinks.rule.engine.cluster.logger.ClusterLogger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import reactor.core.publisher.EmitterProcessor; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import javax.annotation.PostConstruct; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -@Slf4j -@Service -public class RuleEngineDebugService { - - @Autowired - private ExecutableRuleNodeFactory executableRuleNodeFactory; - - @Autowired - private RuleEngineModelParser modelParser; - - @Autowired - private ConditionEvaluator conditionEvaluator; - - private Map sessionStore = new ConcurrentHashMap<>(); - - - public Flux getDebugMessages(String sessionId) { - return getSession(sessionId) - .consumeOutPut(); - } - - private Session getSession(String id) { - return Optional.ofNullable(id) - .map(sessionStore::get) - .orElseThrow(() -> new NotFoundException("session不存在")); - } - - public Mono startSession() { - return Mono.fromSupplier(() -> { - String sessionId = IDGenerator.UUID.generate(); - Session session = new Session(sessionId); - session.local = true; - sessionStore.put(sessionId, session); - return sessionId; - }); - } - - - @SneakyThrows - public String startNode(String sessionId, RuleNodeConfiguration configuration) { - configuration.setNodeType(NodeType.MAP); - Session session = getSession(sessionId); - - DebugExecutionContext context = session.createContext(configuration); - - ExecutableRuleNode ruleNode = executableRuleNodeFactory.create(configuration); - - ruleNode.start(context); - - return context.id; - } - - public void sendData(String sessionId, String contextId, RuleData ruleData) { - getSession(sessionId) - .getContext(contextId) - .execute(ruleData); - } - - - public Mono stopContext(String sessionId, String contextId) { - - return Mono.fromRunnable(() -> getSession(sessionId).stopContext(contextId)) - .thenReturn(true); - } - - public Set getAllContext(String sessionId) { - return getSession(sessionId) - .contexts - .keySet(); - } - - public Mono closeSession(String sessionId) { - return Mono.fromRunnable(() -> getSession(sessionId).close()); - } - - public Mono testCondition(String sessionId, Condition condition, Object data) { - - Session session = getSession(sessionId); - - try { - boolean success = conditionEvaluator.evaluate(condition, RuleData.create(data)); - return session.writeMessage(DebugMessage.of("output", null, "测试条件:".concat(success ? "通过" : "未通过"))); - } catch (Exception e) { - return session.writeMessage(DebugMessage.of("error", null, StringUtils.throwable2String(e))); - } - } - - private class Session { - private String id; - - private long lastOperationTime; - - private Map contexts = new ConcurrentHashMap<>(); - - private Map instanceContext = new ConcurrentHashMap<>(); - - private Map instanceContextMapping = new ConcurrentHashMap<>(); - - private EmitterProcessor messageQueue = EmitterProcessor.create(false); - - @Getter - private boolean local = false; - - private Session(String id) { - this.lastOperationTime = System.currentTimeMillis(); - this.id = id; - } - - private boolean isTimeout() { - return System.currentTimeMillis() - lastOperationTime > TimeUnit.MINUTES.toMillis(15); - } - - private void checkContextTimeout() { - contexts.entrySet() - .stream() - .filter(e -> e.getValue().isTimeout()) - .map(Map.Entry::getKey) - .map(contexts::remove) - .forEach(DebugExecutionContext::stop); - } - - private void stopContext(String contextId) { - Optional.ofNullable(contexts.remove(contextId)) - .ifPresent(ExecutionContext::stop); - } - - private Logger createLogger(String contextId, String nodeId) { - ClusterLogger logger = new ClusterLogger(); - logger.setParent(new Slf4jLogger("rule.engine.debug.".concat(nodeId))); - logger.setNodeId(nodeId); - logger.setInstanceId(contextId); - logger.setLogInfoConsumer(logInfo -> { - - Map data = new HashMap<>(); - data.put("level", logInfo.getLevel()); - data.put("message", logInfo.getMessage()); - - writeMessage(DebugMessage.of("log", contextId, data)) - .subscribe(); - - }); - return logger; - } - - private DebugExecutionContext createContext(RuleNodeConfiguration configuration) { - lastOperationTime = System.currentTimeMillis(); - String id = Optional.ofNullable(configuration.getId()).orElseGet(IDGenerator.MD5::generate); - DebugExecutionContext context = contexts.get(id); - if (context != null) { - context.stop(); - contexts.remove(id); - } - - context = new DebugExecutionContext(id, createLogger(id, configuration.getNodeId()), this); - context.local = true; - contexts.put(id, context); - return context; - } - - private DebugExecutionContext getContext(String id) { - lastOperationTime = System.currentTimeMillis(); - - return contexts.computeIfAbsent(id, _id -> new DebugExecutionContext(id, new Slf4jLogger("rule.engine.debug.none"), this)); - } - - private void execute(RuleData ruleData) { - String instanceId = ruleData.getAttribute("instanceId").map(String::valueOf).orElse(null); - - RuleInstanceContext context = instanceContext.get(instanceId); - if (context != null) { - doExecute(context, ruleData); - } - - } - - private void doExecute(RuleInstanceContext context, RuleData ruleData) { - - context.execute(Mono.just(ruleData)) - .doOnError((throwable) -> { - writeMessage(DebugMessage.of("error", context.getId(), "执行规则失败:" + StringUtils.throwable2String(throwable))); - }) - .subscribe(resp -> { - writeMessage(DebugMessage.of("output", context.getId(), resp.getData())); - }); - - } - - private void execute(ExecuteRuleRequest request) { - - RuleInstanceContext context = instanceContext.get(request.getContextId()); - if (context == null) { - return; - } - RuleData ruleData = RuleData.create(request.getData()); - RuleDataHelper.markStartWith(ruleData, request.getStartWith()); - RuleDataHelper.markSyncReturn(ruleData, request.getEndWith()); - ruleData.setAttribute("debugSessionId", id); - ruleData.setAttribute("instanceId", request.getContextId()); - - doExecute(context, ruleData); - } - - private Mono writeMessage(DebugMessage message) { - lastOperationTime = System.currentTimeMillis(); - return Mono.fromRunnable(() -> messageQueue.onNext(message)) - .thenReturn(true); - } - - - @SneakyThrows - public Flux consumeOutPut() { - - return messageQueue - .map(Function.identity()); - } - - public void close() { - contexts.forEach((s, context) -> context.stop()); - instanceContext.values().forEach(RuleInstanceContext::stop); - instanceContext.clear(); - instanceContextMapping.clear(); - messageQueue.onComplete(); - } - - } - - private class DebugExecutionContext implements ExecutionContext { - - private Session session; - - private String id; - - private EmitterProcessor inputQueue = EmitterProcessor.create(false); - - private Logger logger; - - private List stopListener = new CopyOnWriteArrayList<>(); - - private long lastOperationTime = System.currentTimeMillis(); - - @Getter - private boolean local = false; - - public DebugExecutionContext(String id, Logger logger, Session session) { - this.session = session; - this.logger = logger; - this.id = id; - } - - public boolean isTimeout() { - return System.currentTimeMillis() - lastOperationTime > TimeUnit.MINUTES.toMillis(15); - } - - @Override - public String getInstanceId() { - return id; - } - - @Override - public String getNodeId() { - return id; - } - - @Override - public Logger logger() { - return logger; - } - - public void execute(RuleData ruleData) { - lastOperationTime = System.currentTimeMillis(); - - ruleData.setAttribute("debug", true); - inputQueue.onNext(ruleData); - } - - @Override - public Input getInput() { - - return new Input() { - @Override - public Flux subscribe() { - return inputQueue.map(Function.identity()) - .doOnNext(data -> { - log.debug("handle input :{}", data); - }) - .doFinally(s -> { - log.debug("unsubscribe input:{}", id); - }); - } - - @Override - public void close() { - inputQueue.onComplete(); - } - }; - } - - @Override - public Output getOutput() { - return (data) -> Flux.from(data) - .flatMap(d -> session.writeMessage(DebugMessage.of("output", id, JSON.toJSONString(d.getData(), SerializerFeature.PrettyFormat)))) - .then(Mono.just(true)); - } - - @Override - public Mono fireEvent(String event, RuleData data) { - return Mono.empty(); - } - - @Override - public Mono onError(RuleData data, Throwable e) { - return session - .writeMessage(DebugMessage.of("error", id, StringUtils.throwable2String(e))) - .then(); - } - - @Override - public void stop() { - stopListener.forEach(Runnable::run); - } - - @Override - public void onStop(Runnable runnable) { - stopListener.add(runnable); - } - } - - -} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/RuleInstanceService.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/RuleInstanceService.java index 8762a5dc..fe5741c3 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/RuleInstanceService.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/RuleInstanceService.java @@ -4,16 +4,15 @@ import lombok.extern.slf4j.Slf4j; import org.hswebframework.ezorm.core.param.QueryParam; import org.hswebframework.web.api.crud.entity.PagerResult; import org.hswebframework.web.crud.service.GenericReactiveCrudService; -import org.jetlinks.community.rule.engine.entity.RuleEngineExecuteLogInfo; -import org.jetlinks.community.rule.engine.entity.RuleEngineExecuteEventInfo; -import org.jetlinks.community.rule.engine.event.handler.RuleEngineLoggerIndexProvider; import org.jetlinks.community.elastic.search.service.ElasticSearchService; -import org.jetlinks.community.rule.engine.enums.RuleInstanceState; +import org.jetlinks.community.rule.engine.entity.RuleEngineExecuteEventInfo; +import org.jetlinks.community.rule.engine.entity.RuleEngineExecuteLogInfo; import org.jetlinks.community.rule.engine.entity.RuleInstanceEntity; -import org.jetlinks.rule.engine.api.Rule; +import org.jetlinks.community.rule.engine.enums.RuleInstanceState; +import org.jetlinks.community.rule.engine.event.handler.RuleEngineLoggerIndexProvider; import org.jetlinks.rule.engine.api.RuleEngine; -import org.jetlinks.rule.engine.api.RuleInstanceContext; import org.jetlinks.rule.engine.api.model.RuleEngineModelParser; +import org.jetlinks.rule.engine.api.model.RuleModel; import org.reactivestreams.Publisher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; @@ -44,30 +43,29 @@ public class RuleInstanceService extends GenericReactiveCrudService stop(String id) { return this.ruleEngine - .getInstance(id) - .flatMap(RuleInstanceContext::stop) - .switchIfEmpty(Mono.empty()) - .then(createUpdate() - .set(RuleInstanceEntity::getState, RuleInstanceState.stopped) - .where(RuleInstanceEntity::getId,id) - .execute()) - .then(); + .shutdown(id) + .then(createUpdate() + .set(RuleInstanceEntity::getState, RuleInstanceState.stopped) + .where(RuleInstanceEntity::getId, id) + .execute()) + .then(); } - public Mono start(String id) { + public Mono start(String id) { return findById(Mono.just(id)) - .flatMap(this::doStart); + .flatMapMany(instance -> this.ruleEngine.startRule(id, instance.toRule(modelParser))) + .then(); } - private Mono doStart(RuleInstanceEntity entity) { + private Mono doStart(RuleInstanceEntity entity) { return Mono.defer(() -> { - Rule rule = entity.toRule(modelParser); - return ruleEngine.startRule(rule) - .flatMap(ctx -> createUpdate() - .set(RuleInstanceEntity::getState, RuleInstanceState.started) - .where(entity::getId) - .execute() - .thenReturn(ctx)); + RuleModel model = entity.toRule(modelParser); + return ruleEngine + .startRule(entity.getId(), model) + .then(createUpdate() + .set(RuleInstanceEntity::getState, RuleInstanceState.started) + .where(entity::getId) + .execute()).then(); }); } @@ -81,12 +79,10 @@ public class RuleInstanceService extends GenericReactiveCrudService { - log.debug("start rule {}", context.getId()); - }); + .where() + .is(RuleInstanceEntity::getState, RuleInstanceState.started) + .fetch() + .flatMap(this::doStart) + .subscribe(); } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleEngineDebugController.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleEngineDebugController.java deleted file mode 100644 index 6d55a15d..00000000 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleEngineDebugController.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.jetlinks.community.rule.engine.web; - -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; -import org.hswebframework.web.authorization.annotation.Authorize; -import org.hswebframework.web.authorization.annotation.Resource; -import org.jetlinks.community.rule.engine.service.DebugMessage; -import org.jetlinks.community.rule.engine.service.RuleEngineDebugService; -import org.jetlinks.rule.engine.api.RuleData; -import org.jetlinks.rule.engine.api.executor.RuleNodeConfiguration; -import org.jetlinks.rule.engine.api.model.Condition; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.*; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -@RestController -@RequestMapping("/rule-engine/debug") -@Authorize -@Resource(id="rule-model",name = "规则引擎-模型") -public class RuleEngineDebugController { - - @Autowired - private RuleEngineDebugService ruleDebugService; - - @PostMapping - public Mono startSession() { - return ruleDebugService.startSession(); - } - - @PostMapping("/{sessionId}") - public Mono startNode(@PathVariable String sessionId, @RequestBody RuleNodeConfiguration configuration) { - return Mono.just(ruleDebugService.startNode(sessionId, configuration)); - } - - @GetMapping(value = "/{sessionId}/logs", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public Flux getSessionLogs(@PathVariable String sessionId) { - return ruleDebugService.getDebugMessages(sessionId); - } - - @PostMapping("/{sessionId}/{contextId}") - public Mono executeNode(@PathVariable String sessionId, - @PathVariable String contextId, - @RequestBody String data) { - Object object = data.trim(); - if (data.startsWith("[") || data.startsWith("{")) { - object = JSON.parse(data); - } - - RuleData ruleData = RuleData.create(object); - - ruleDebugService.sendData(sessionId, contextId, ruleData); - - return Mono.just(ruleData.getId()); - } - - @GetMapping("/{sessionId}/contexts") - public Flux getSessionContexts(@PathVariable String sessionId) { - return Flux.fromIterable(ruleDebugService.getAllContext(sessionId)); - } - - - @PostMapping("/{sessionId}/condition") - public Mono testCondition(@PathVariable String sessionId, @RequestBody JSONObject data) { - return ruleDebugService.testCondition(sessionId, data.getJSONObject("condition").toJavaObject(Condition.class), data.get("data")); - } - - @DeleteMapping("/{sessionId}") - public Mono stopSession(@PathVariable String sessionId) { - return ruleDebugService.closeSession(sessionId); - } - - @DeleteMapping("/{sessionId}/{contextId}") - public Mono stopContext(@PathVariable String sessionId, @PathVariable String contextId) { - - return ruleDebugService.stopContext(sessionId, contextId); - } - -} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleInstanceController.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleInstanceController.java index e7a80a7f..ef1fbd71 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleInstanceController.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleInstanceController.java @@ -7,18 +7,12 @@ import org.hswebframework.web.authorization.annotation.Resource; import org.hswebframework.web.authorization.annotation.ResourceAction; import org.hswebframework.web.crud.service.ReactiveCrudService; import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController; -import org.hswebframework.web.exception.NotFoundException; -import org.hswebframework.web.id.IDGenerator; -import org.jetlinks.community.rule.engine.entity.RuleEngineExecuteLogInfo; import org.jetlinks.community.rule.engine.entity.RuleEngineExecuteEventInfo; +import org.jetlinks.community.rule.engine.entity.RuleEngineExecuteLogInfo; import org.jetlinks.community.rule.engine.entity.RuleInstanceEntity; import org.jetlinks.community.rule.engine.service.RuleInstanceService; -import org.jetlinks.rule.engine.api.DefaultRuleData; -import org.jetlinks.rule.engine.api.RuleDataHelper; -import org.jetlinks.rule.engine.api.RuleEngine; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @RestController @@ -29,8 +23,6 @@ public class RuleInstanceController implements ReactiveServiceCrudController execute(@PathVariable String id, - @PathVariable String startWith, - @PathVariable String endWith, - @RequestBody Flux payload) { - return ruleEngine - .getInstance(id) - .switchIfEmpty(Mono.error(NotFoundException::new)) - .flatMapMany(context -> context - .execute(payload - .map(ruleData -> { - ruleData.setId(IDGenerator.SNOW_FLAKE_STRING.generate()); - RuleDataHelper.markStartWith(ruleData, startWith); - return RuleDataHelper.markSyncReturn(ruleData, endWith); - }) - )); - } - @Override public ReactiveCrudService getService() { return instanceService; diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleModelController.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleModelController.java index 3f28f787..caeede1c 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleModelController.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleModelController.java @@ -7,12 +7,15 @@ import org.hswebframework.web.crud.service.ReactiveCrudService; import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController; import org.jetlinks.community.rule.engine.entity.RuleModelEntity; import org.jetlinks.community.rule.engine.service.RuleModelService; -import org.jetlinks.rule.engine.api.executor.ExecutableRuleNodeFactory; +import org.jetlinks.rule.engine.api.RuleEngine; +import org.jetlinks.rule.engine.api.worker.Worker; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.function.Function; + @RestController @RequestMapping("rule-engine/model") @Resource(id = "rule-model", name = "规则引擎-模型") @@ -22,7 +25,7 @@ public class RuleModelController implements ReactiveServiceCrudController getService() { @@ -39,6 +42,6 @@ public class RuleModelController implements ReactiveServiceCrudController getAllSupportExecutors() { - return Flux.fromIterable(factory.getAllSupportExecutor()); + return ruleEngine.getWorkers().flatMap(Worker::getSupportExecutors).flatMapIterable(Function.identity()); } } diff --git a/jetlinks-standalone/src/main/java/org/jetlinks/community/standalone/JetLinksApplication.java b/jetlinks-standalone/src/main/java/org/jetlinks/community/standalone/JetLinksApplication.java index 7e9f11c4..ebf32c67 100644 --- a/jetlinks-standalone/src/main/java/org/jetlinks/community/standalone/JetLinksApplication.java +++ b/jetlinks-standalone/src/main/java/org/jetlinks/community/standalone/JetLinksApplication.java @@ -8,6 +8,7 @@ import org.hswebframework.web.loggin.aop.EnableAccessLogger; import org.hswebframework.web.logging.events.AccessLoggerAfterEvent; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Profile; @@ -18,7 +19,8 @@ import javax.annotation.PostConstruct; @SpringBootApplication(scanBasePackages = "org.jetlinks.community", exclude = { - DataSourceAutoConfiguration.class + DataSourceAutoConfiguration.class, + RestClientAutoConfiguration.class }) @EnableCaching @EnableEasyormRepository("org.jetlinks.community.**.entity") diff --git a/jetlinks-standalone/src/main/java/org/jetlinks/community/standalone/configuration/ExecutorConfiguration.java b/jetlinks-standalone/src/main/java/org/jetlinks/community/standalone/configuration/ExecutorConfiguration.java index ca3b591f..db39011e 100644 --- a/jetlinks-standalone/src/main/java/org/jetlinks/community/standalone/configuration/ExecutorConfiguration.java +++ b/jetlinks-standalone/src/main/java/org/jetlinks/community/standalone/configuration/ExecutorConfiguration.java @@ -10,6 +10,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.concurrent.ThreadPoolExecutorFactoryBean; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; import java.util.concurrent.*; @@ -34,6 +36,11 @@ public class ExecutorConfiguration { }; } + @Bean + public Scheduler reactorScheduler(ScheduledExecutorService executorService) { + return Schedulers.fromExecutorService(executorService); + } + @Bean @ConfigurationProperties(prefix = "jetlinks.executor") @Primary diff --git a/jetlinks-standalone/src/main/java/org/jetlinks/community/standalone/web/SystemInfoController.java b/jetlinks-standalone/src/main/java/org/jetlinks/community/standalone/web/SystemInfoController.java new file mode 100644 index 00000000..06362a1a --- /dev/null +++ b/jetlinks-standalone/src/main/java/org/jetlinks/community/standalone/web/SystemInfoController.java @@ -0,0 +1,20 @@ +package org.jetlinks.community.standalone.web; + +import org.hswebframework.web.authorization.annotation.Authorize; +import org.jetlinks.community.Version; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@RequestMapping("/system") +@RestController +public class SystemInfoController { + + @GetMapping("/version") + @Authorize(ignore = true) + public Mono getVersion() { + return Mono.just(Version.current); + } + +} diff --git a/pom.xml b/pom.xml index 5015b122..1b759ed6 100644 --- a/pom.xml +++ b/pom.xml @@ -16,19 +16,20 @@ UTF-8 zh_CN - 2.2.6.RELEASE + 2.2.8.RELEASE 1.8 ${java.version} - 4.0.3 - 4.0.3 + 4.0.4-SNAPSHOT + 4.0.4-SNAPSHOT 3.0.2 - 1.0.4-SNAPSHOT + 1.1.0-SNAPSHOT Arabba-SR3 3.8.5 - 4.1.46.Final + 4.1.50.Final 6.8.6 1.0-RC 1.0.0 + 1.2.70 @@ -150,8 +151,22 @@ 3.0.0 + + + + build + + + maven-central + central + https://repo1.maven.org/maven2/ + + + + + @@ -182,7 +197,7 @@ com.alibaba fastjson - 1.2.56 + ${fastjson.version} @@ -374,16 +389,6 @@ https://repo.spring.io/milestone - - - - - - - - - -