同步2.0相关功能代码 (#206)

This commit is contained in:
bestfeng1020 2022-10-21 19:06:52 +08:00 committed by GitHub
parent 1a25e693b9
commit f408d72159
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 5282 additions and 450 deletions

View File

@ -0,0 +1,27 @@
package org.jetlinks.community.doc;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.ezorm.core.param.Term;
import java.util.List;
/**
* 文档专用,描述仅有查询功能的动态查询参数
*
* @author zhouhao
* @since 1.5
* @see org.hswebframework.web.api.crud.entity.QueryParamEntity
*/
@Getter
@Setter
public class QueryConditionOnly {
@Schema(description = "where条件表达式,与terms参数不能共存.语法: name = 张三 and age > 16")
private String where;
@Schema(description = "查询条件集合")
private List<Term> terms;
}

View File

@ -0,0 +1,19 @@
package org.jetlinks.community.micrometer;
/**
* 监控指标自定义注册接口,用于对指标进行自定义,如添加指标标签等操作
*
* @author zhouhao
* @since 1.11
*/
public interface MeterRegistryCustomizer {
/**
* 在指标首次初始化时调用,可以通过判断metric进行自定义标签
*
* @param metric 指标
* @param settings 自定义设置
*/
void custom(String metric, MeterRegistrySettings settings);
}

View File

@ -0,0 +1,23 @@
package org.jetlinks.community.micrometer;
import org.jetlinks.core.metadata.DataType;
import javax.annotation.Nonnull;
/**
* 指标注册配置信息
*
* @author zhouhao
* @since 1.11
*/
public interface MeterRegistrySettings {
/**
* 给指标添加标签,用于自定义标签类型.在相应指标实现中会根据类型对数据进行存储
*
* @param tag 标签key
* @param type 类型
*/
void addTag(@Nonnull String tag, @Nonnull DataType type);
}

View File

@ -6,6 +6,8 @@ import org.hswebframework.ezorm.rdb.exception.DuplicateKeyException;
import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata;
import org.hswebframework.web.crud.web.ResponseMessage;
import org.hswebframework.web.i18n.LocaleUtils;
import org.jetlinks.community.reference.DataReferenceInfo;
import org.jetlinks.community.reference.DataReferencedException;
import org.jetlinks.core.enums.ErrorCode;
import org.jetlinks.core.exception.DeviceOperationException;
import org.springframework.core.Ordered;
@ -102,5 +104,15 @@ public class ErrorControllerAdvice {
.body(ResponseMessage.error(status.value(), e.getCode().name().toLowerCase(), msg))
);
}
@ExceptionHandler
public Mono<ResponseMessage<List<DataReferenceInfo>>> handleException(DataReferencedException e) {
return e
.getLocalizedMessageReactive()
.map(msg -> {
return ResponseMessage
.<List<DataReferenceInfo>>error(400,"error.data.referenced", msg)
.result(e.getReferenceList());
});
}
}

View File

@ -1,3 +1,4 @@
message.device_message_handing=Message sent to device, processing...
error.duplicate_key_detail=Duplicate Data:{0}
error.duplicate_key_detail=Duplicate Data:{0}
error.data.referenced=The data has been used elsewhere

View File

@ -1,3 +1,3 @@
message.device_message_handing=消息已发往设备,处理中...
error.data.referenced=数据已经被其他地方使用
error.duplicate_key_detail=重复的数据:{0}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>notify-component</artifactId>
<groupId>org.jetlinks.community</groupId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>notify-voice</artifactId>
<dependencies>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.2</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>notify-core</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,19 @@
package org.jetlinks.community.notify.voice;
import org.jetlinks.community.notify.template.TemplateManager;
import org.jetlinks.community.notify.voice.aliyun.AliyunNotifierProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class VoiceNotifierConfiguration {
@Bean
@ConditionalOnBean(TemplateManager.class)
public AliyunNotifierProvider aliyunNotifierProvider(TemplateManager templateManager) {
return new AliyunNotifierProvider(templateManager);
}
}

View File

@ -0,0 +1,20 @@
package org.jetlinks.community.notify.voice;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.jetlinks.community.notify.Provider;
@Getter
@AllArgsConstructor
public enum VoiceProvider implements Provider {
aliyun("阿里云")
;
private final String name;
@Override
public String getId() {
return name();
}
}

View File

@ -0,0 +1,80 @@
package org.jetlinks.community.notify.voice.aliyun;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.i18n.LocaleUtils;
import org.jetlinks.community.notify.*;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.jetlinks.core.metadata.DefaultConfigMetadata;
import org.jetlinks.core.metadata.types.IntType;
import org.jetlinks.core.metadata.types.StringType;
import org.jetlinks.community.notify.template.TemplateManager;
import org.jetlinks.community.notify.template.TemplateProperties;
import org.jetlinks.community.notify.template.TemplateProvider;
import org.jetlinks.community.notify.voice.VoiceProvider;
import reactor.core.publisher.Mono;
import javax.annotation.Nonnull;
/**
* <a href="https://help.aliyun.com/document_detail/114035.html?spm=a2c4g.11186623.6.561.3d1b3c2dGMXAmk">
* 阿里云语音通知服务
* </a>
*
* @author zhouhao
* @since 1.0
*/
@Slf4j
@AllArgsConstructor
public class AliyunNotifierProvider implements NotifierProvider, TemplateProvider {
private TemplateManager templateManager;
@Nonnull
@Override
public Provider getProvider() {
return VoiceProvider.aliyun;
}
public static final DefaultConfigMetadata templateConfig = new DefaultConfigMetadata("阿里云语音模版",
"https://help.aliyun.com/document_detail/114035.html?spm=a2c4g.11186623.6.561.3d1b3c2dGMXAmk")
.add("ttsCode", "模版ID", "ttsCode", new StringType())
.add("calledShowNumbers", "被叫显号", "", new StringType())
.add("CalledNumber", "被叫号码", "", new StringType())
.add("PlayTimes", "播放次数", "", new IntType());
public static final DefaultConfigMetadata notifierConfig = new DefaultConfigMetadata("阿里云通知配置",
"https://help.aliyun.com/document_detail/114035.html?spm=a2c4g.11186623.6.561.3d1b3c2dGMXAmk")
.add("regionId", "regionId", "regionId", new StringType())
.add("accessKeyId", "accessKeyId", "", new StringType())
.add("secret", "secret", "", new StringType());
@Override
public ConfigMetadata getTemplateConfigMetadata() {
return templateConfig;
}
@Override
public ConfigMetadata getNotifierConfigMetadata() {
return notifierConfig;
}
@Override
public Mono<AliyunVoiceTemplate> createTemplate(TemplateProperties properties) {
return Mono.fromCallable(() -> new AliyunVoiceTemplate().with(properties).validate())
.as(LocaleUtils::transform);
}
@Nonnull
@Override
public NotifyType getType() {
return DefaultNotifyType.voice;
}
@Nonnull
@Override
public Mono<AliyunVoiceNotifier> createNotifier(@Nonnull NotifierProperties properties) {
return Mono.fromSupplier(() -> new AliyunVoiceNotifier(properties, templateManager))
.as(LocaleUtils::transform);
}
}

View File

@ -0,0 +1,116 @@
package org.jetlinks.community.notify.voice.aliyun;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.exception.BusinessException;
import org.hswebframework.web.logger.ReactiveLogger;
import org.jetlinks.community.notify.*;
import org.jetlinks.community.notify.template.TemplateManager;
import org.jetlinks.community.notify.voice.VoiceProvider;
import org.jetlinks.core.Values;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import javax.annotation.Nonnull;
import java.util.Map;
import java.util.Objects;
@Slf4j
public class AliyunVoiceNotifier extends AbstractNotifier<AliyunVoiceTemplate> {
private final IAcsClient client;
private final int connectTimeout = 1000;
private final int readTimeout = 5000;
@Getter
private String notifierId;
private String domain = "dyvmsapi.aliyuncs.com";
private String regionId = "cn-hangzhou";
public AliyunVoiceNotifier(NotifierProperties profile, TemplateManager templateManager) {
super(templateManager);
Map<String, Object> config = profile.getConfiguration();
DefaultProfile defaultProfile = DefaultProfile.getProfile(
this.regionId = (String) Objects.requireNonNull(config.get("regionId"), "regionId不能为空"),
(String) Objects.requireNonNull(config.get("accessKeyId"), "accessKeyId不能为空"),
(String) Objects.requireNonNull(config.get("secret"), "secret不能为空")
);
this.client = new DefaultAcsClient(defaultProfile);
this.domain = (String) config.getOrDefault("domain", "dyvmsapi.aliyuncs.com");
this.notifierId = profile.getId();
}
public AliyunVoiceNotifier(IClientProfile profile, TemplateManager templateManager) {
this(new DefaultAcsClient(profile), templateManager);
}
public AliyunVoiceNotifier(IAcsClient client, TemplateManager templateManager) {
super(templateManager);
this.client = client;
}
@Override
@Nonnull
public NotifyType getType() {
return DefaultNotifyType.voice;
}
@Nonnull
@Override
public Provider getProvider() {
return VoiceProvider.aliyun;
}
@Override
@Nonnull
public Mono<Void> send(@Nonnull AliyunVoiceTemplate template, @Nonnull Values context) {
return Mono.<Void>defer(() -> {
try {
CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysDomain(domain);
request.setSysVersion("2017-05-25");
request.setSysAction("SingleCallByTts");
request.setSysConnectTimeout(connectTimeout);
request.setSysReadTimeout(readTimeout);
request.putQueryParameter("RegionId", regionId);
request.putQueryParameter("CalledShowNumber", template.getCalledShowNumbers());
request.putQueryParameter("CalledNumber", template.getCalledNumber(context.getAllValues()));
request.putQueryParameter("TtsCode", template.getTtsCode());
request.putQueryParameter("PlayTimes", String.valueOf(template.getPlayTimes()));
request.putQueryParameter("TtsParam", template.createTtsParam(context.getAllValues()));
CommonResponse response = client.getCommonResponse(request);
log.info("发起语音通知完成 {}:{}", response.getHttpResponse().getStatus(), response.getData());
JSONObject json = JSON.parseObject(response.getData());
if (!"ok".equalsIgnoreCase(json.getString("Code"))) {
return Mono.error(new BusinessException(json.getString("Message"), json.getString("Code")));
}
} catch (Exception e) {
return Mono.error(e);
}
return Mono.empty();
}).doOnEach(ReactiveLogger.onError(err -> {
log.info("发起语音通知失败", err);
})).subscribeOn(Schedulers.boundedElastic());
}
@Override
@Nonnull
public Mono<Void> close() {
return Mono.fromRunnable(client::shutdown);
}
}

View File

@ -0,0 +1,68 @@
package org.jetlinks.community.notify.voice.aliyun;
import com.alibaba.fastjson.JSON;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.jetlinks.community.notify.template.AbstractTemplate;
import org.jetlinks.community.notify.template.Template;
import org.jetlinks.community.notify.template.VariableDefinition;
import org.springframework.util.StringUtils;
import javax.annotation.Nonnull;
import javax.validation.constraints.NotBlank;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* 阿里云语音验证码通知模版
* <p>
* https://help.aliyun.com/document_detail/114035.html?spm=a2c4g.11186623.6.561.3d1b3c2dGMXAmk
*/
@Getter
@Setter
public class AliyunVoiceTemplate extends AbstractTemplate<AliyunVoiceTemplate> {
public static final String CALLED_NUMBER_KEY = "calledNumber";
@Schema(description = "通知模版ID")
@NotBlank(message = "[ttsCode]不能为空")
private String ttsCode;
private String calledShowNumbers;
@NotBlank(message = "[calledNumber]不能为空")
private String calledNumber;
@Schema(description = "通知播放次数")
private int playTimes = 1;
private Map<String, String> ttsParam;
public String createTtsParam(Map<String, Object> ctx) {
return JSON.toJSONString(ctx);
}
public String getCalledNumber(Map<String, Object> ctx) {
return get(CALLED_NUMBER_KEY, ctx, this::getCalledNumber);
}
@Nonnull
@Override
protected List<VariableDefinition> getEmbeddedVariables() {
//指定了固定的收信人
if (StringUtils.hasText(calledNumber)) {
return Collections.emptyList();
}
return Collections.singletonList(
VariableDefinition
.builder()
.id(CALLED_NUMBER_KEY)
.name("收信人")
.description("收信人手机号码")
.required(true)
.build()
);
}
}

View File

@ -0,0 +1,20 @@
package org.jetlinks.community.notify.voice;
import org.jetlinks.community.notify.voice.aliyun.AliyunNotifierProvider;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
* 输入描述.
*
* @author zhangji
* @version 1.11 2022/2/7
*/
public class VoiceNotifierConfigurationTest {
@Test
void test() {
VoiceNotifierConfiguration configuration = new VoiceNotifierConfiguration();
AliyunNotifierProvider provider = configuration.aliyunNotifierProvider(null);
Assertions.assertNotNull(provider);
}
}

View File

@ -0,0 +1,90 @@
package org.jetlinks.community.notify.voice.aliyun;
import com.alibaba.fastjson.JSONObject;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.jetlinks.community.notify.DefaultNotifyType;
import org.jetlinks.community.notify.NotifierProperties;
import org.jetlinks.community.notify.template.TemplateProperties;
import org.jetlinks.community.notify.voice.VoiceProvider;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import java.util.HashMap;
import java.util.Map;
/**
* 输入描述.
*
* @author zhangji
* @version 1.11 2022/2/7
*/
public class AliyunNotifierProviderTest {
private AliyunNotifierProvider provider;
private TemplateProperties templateProperties;
private NotifierProperties notifierProperties;
private static final String TTS_CODE = "ttsCode";
private static final String NOTIFIER_ID = "notifier_id";
@BeforeEach
void init() {
provider = new AliyunNotifierProvider(null);
templateProperties = new TemplateProperties();
templateProperties.setId("test");
templateProperties.setType(DefaultNotifyType.voice.getId());
templateProperties.setProvider(VoiceProvider.aliyun.getId());
AliyunVoiceTemplate aliyunVoiceTemplate = new AliyunVoiceTemplate();
aliyunVoiceTemplate.setTtsCode(TTS_CODE);
aliyunVoiceTemplate.setCalledNumber("calledNumber");
templateProperties.setTemplate((JSONObject)JSONObject.toJSON(aliyunVoiceTemplate));
notifierProperties = new NotifierProperties();
notifierProperties.setId(NOTIFIER_ID);
notifierProperties.setType(DefaultNotifyType.voice.getId());
notifierProperties.setProvider(VoiceProvider.aliyun.getId());
Map<String, Object> config = new HashMap<>();
config.put("regionId", "regionId");
config.put("accessKeyId", "accessKeyId");
config.put("secret", "secret");
notifierProperties.setConfiguration(config);
}
@Test
void test() {
Assertions.assertEquals(VoiceProvider.aliyun, provider.getProvider());
Assertions.assertEquals(DefaultNotifyType.voice, provider.getType());
}
@Test
void getTemplateConfigMetadata() {
ConfigMetadata templateConfig = provider.getTemplateConfigMetadata();
Assertions.assertNotNull(templateConfig);
Assertions.assertEquals("阿里云语音模版", templateConfig.getName());
}
@Test
void getNotifierConfigMetadata() {
ConfigMetadata notifierConfig = provider.getNotifierConfigMetadata();
Assertions.assertNotNull(notifierConfig);
Assertions.assertEquals("阿里云通知配置", notifierConfig.getName());
}
@Test
void createTemplate() {
provider.createTemplate(templateProperties)
.as(StepVerifier::create)
.expectNextMatches(template -> template.getTtsCode().equals(TTS_CODE))
.verifyComplete();
}
@Test
void createNotifier() {
provider.createNotifier(notifierProperties)
.as(StepVerifier::create)
.expectNextMatches(notifier -> notifier.getNotifierId().equals(NOTIFIER_ID))
.verifyComplete();
}
}

View File

@ -0,0 +1,69 @@
package org.jetlinks.community.notify.voice.aliyun;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.HttpResponse;
import org.jetlinks.core.Values;
import org.jetlinks.community.notify.DefaultNotifyType;
import org.jetlinks.community.notify.voice.VoiceProvider;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.http.HttpStatus;
import reactor.test.StepVerifier;
import java.util.HashMap;
/**
* 输入描述.
*
* @author zhangji
* @version 1.11 2022/2/7
*/
public class AliyunVoiceNotifierTest {
private IAcsClient client;
private AliyunVoiceNotifier notifier;
private static final String TTS_CODE = "tts_code";
@BeforeEach
void init() throws ClientException {
client = Mockito.mock(IAcsClient.class);
CommonResponse response = new CommonResponse();
HttpResponse httpResponse = new HttpResponse();
httpResponse.setStatus(HttpStatus.OK.value());
response.setHttpResponse(httpResponse);
response.setData("{\"Code\":\"ok\"}");
Mockito.when(client.getCommonResponse(Mockito.any(CommonRequest.class))).thenReturn(response);
notifier = new AliyunVoiceNotifier(client, null);
}
@Test
void test() {
Assertions.assertNotNull(notifier);
Assertions.assertEquals(DefaultNotifyType.voice, notifier.getType());
Assertions.assertEquals(VoiceProvider.aliyun, notifier.getProvider());
}
@Test
void send() {
AliyunVoiceTemplate template = new AliyunVoiceTemplate();
template.setTtsCode(TTS_CODE);
template.setCalledNumber("calledNumber");
notifier.send(template, Values.of(new HashMap<>()))
.as(StepVerifier::create)
.verifyComplete();
}
@Test
void close() {
notifier.close()
.as(StepVerifier::create)
.verifyComplete();
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>notify-component</artifactId>
<groupId>org.jetlinks.community</groupId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>notify-webhook</artifactId>
<dependencies>
<dependency>
<groupId>org.jetlinks.community</groupId>
<artifactId>notify-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,21 @@
package org.jetlinks.community.notify.webhook;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.jetlinks.community.notify.Provider;
@Getter
@AllArgsConstructor
public enum WebHookProvider implements Provider {
http("HTTP")
;
private final String name;
@Override
public String getId() {
return name();
}
}

View File

@ -0,0 +1,96 @@
package org.jetlinks.community.notify.webhook.http;
import org.apache.commons.collections.CollectionUtils;
import org.jetlinks.community.notify.AbstractNotifier;
import org.jetlinks.community.notify.DefaultNotifyType;
import org.jetlinks.community.notify.NotifyType;
import org.jetlinks.community.notify.Provider;
import org.jetlinks.community.notify.template.TemplateManager;
import org.jetlinks.community.notify.webhook.WebHookProvider;
import org.jetlinks.core.Values;
import org.springframework.http.HttpMethod;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import javax.annotation.Nonnull;
public class HttpWebHookNotifier extends AbstractNotifier<HttpWebHookTemplate> {
private final String id;
private final WebClient webClient;
private final HttpWebHookProperties properties;
public HttpWebHookNotifier(String id,
HttpWebHookProperties properties,
WebClient webClient,
TemplateManager templateManager) {
super(templateManager);
this.id = id;
this.properties = properties;
this.webClient = webClient;
}
@Override
public String getNotifierId() {
return id;
}
@Nonnull
@Override
public NotifyType getType() {
return DefaultNotifyType.webhook;
}
@Nonnull
@Override
public Provider getProvider() {
return WebHookProvider.http;
}
@Nonnull
@Override
public Mono<Void> send(@Nonnull HttpWebHookTemplate template,
@Nonnull Values context) {
HttpMethod method = template.getMethod();
WebClient.RequestBodyUriSpec bodyUriSpec = webClient
.method(template.getMethod());
if (StringUtils.hasText(template.getUrl())) {
bodyUriSpec.uri(template.getUrl());
}
if (method == HttpMethod.POST
|| method == HttpMethod.PUT
|| method == HttpMethod.PATCH) {
String body = template.resolveBody(context);
if (null != body) {
bodyUriSpec.bodyValue(body);
}
}
bodyUriSpec.headers(headers -> {
if (CollectionUtils.isNotEmpty(properties.getHeaders())) {
for (HttpWebHookProperties.Header header : properties.getHeaders()) {
headers.add(header.getKey(), header.getValue());
}
}
if (CollectionUtils.isNotEmpty(template.getHeaders())) {
for (HttpWebHookProperties.Header header : template.getHeaders()) {
headers.add(header.getKey(), header.getValue());
}
}
});
return bodyUriSpec
.retrieve()
.bodyToMono(Void.class);
}
@Nonnull
@Override
public Mono<Void> close() {
return Mono.empty();
}
}

View File

@ -0,0 +1,62 @@
package org.jetlinks.community.notify.webhook.http;
import lombok.AllArgsConstructor;
import org.hswebframework.web.bean.FastBeanCopier;
import org.hswebframework.web.i18n.LocaleUtils;
import org.hswebframework.web.validator.ValidatorUtils;
import org.jetlinks.community.notify.*;
import org.jetlinks.community.notify.template.Template;
import org.jetlinks.community.notify.template.TemplateManager;
import org.jetlinks.community.notify.template.TemplateProperties;
import org.jetlinks.community.notify.template.TemplateProvider;
import org.jetlinks.community.notify.webhook.WebHookProvider;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import javax.annotation.Nonnull;
@Component
@AllArgsConstructor
public class HttpWebHookNotifierProvider implements NotifierProvider, TemplateProvider {
private final TemplateManager templateManager;
private final WebClient.Builder builder;
@Nonnull
@Override
public NotifyType getType() {
return DefaultNotifyType.webhook;
}
@Nonnull
@Override
public Provider getProvider() {
return WebHookProvider.http;
}
@Override
public Mono<HttpWebHookTemplate> createTemplate(TemplateProperties properties) {
return Mono.just(new HttpWebHookTemplate().with(properties).validate())
.as(LocaleUtils::transform);
}
@Nonnull
@Override
public Mono<? extends Notifier<? extends Template>> createNotifier(@Nonnull NotifierProperties properties) {
HttpWebHookProperties hookProperties = FastBeanCopier.copy(properties.getConfiguration(),new HttpWebHookProperties());
ValidatorUtils.tryValidate(hookProperties);
WebClient.Builder client = builder.clone();
client.baseUrl(hookProperties.getUrl());
return Mono.just(new HttpWebHookNotifier(properties.getId(),
hookProperties,
client.build(),
templateManager))
.as(LocaleUtils::transform);
}
}

View File

@ -0,0 +1,32 @@
package org.jetlinks.community.notify.webhook.http;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.NotBlank;
import java.util.List;
@Getter
@Setter
public class HttpWebHookProperties {
@Schema(description = "请求根地址,如: https://host/api")
@NotBlank
@URL
private String url;
@Schema(description = "请求头")
private List<Header> headers;
//todo 认证方式
@Getter
@Setter
public static class Header {
private String key;
private String value;
}
}

View File

@ -0,0 +1,108 @@
package org.jetlinks.community.notify.webhook.http;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.google.common.collect.Maps;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.jetlinks.community.notify.template.AbstractTemplate;
import org.jetlinks.core.Values;
import org.springframework.http.HttpMethod;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Getter
@Setter
public class HttpWebHookTemplate extends AbstractTemplate<HttpWebHookTemplate> {
@Schema(description = "请求地址")
private String url = "";
@Schema(description = "请求头")
private List<HttpWebHookProperties.Header> headers;
@Schema(description = "请求方式,默认POST")
private HttpMethod method = HttpMethod.POST;
@Schema(description = "使用上下文作为请求体")
private boolean contextAsBody;
@Schema(description = "自定义请求体")
private String body;
@Hidden
private Boolean bodyIsJson;
//todo 增加认证类型, oauth2等
public String resolveBody(Values context) {
if (!StringUtils.hasText(body)) {
return body;
}
if (contextAsBody) {
return JSON.toJSONString(context.getAllValues());
}
Map<String, Object> contextVal = context.getAllValues();
try {
if (bodyIsJson == null || bodyIsJson) {
//解析为json再填充变量,防止变量值中含有",{等字符时导致json格式错误
String _body = JSON.toJSONString(
resolveBody(JSON.parse(body), contextVal)
);
bodyIsJson = true;
return _body;
}
} catch (JSONException ignore) {
}
bodyIsJson = false;
return render(body, contextVal);
}
private Object resolveBody(Object val, Map<String, Object> context) {
//字符串,支持变量:${var}
if (val instanceof String) {
return render(String.valueOf(val), context);
}
if (val instanceof Map) {
return resolveBody(((Map<?, ?>) val), context);
}
if (val instanceof List) {
return resolveBody(((List<?>) val), context);
}
return val;
}
private Map<Object, Object> resolveBody(Map<?, ?> obj, Map<String, Object> context) {
Map<Object, Object> val = Maps.newLinkedHashMapWithExpectedSize(obj.size());
for (Map.Entry<?, ?> entry : obj.entrySet()) {
Object key = resolveBody(entry.getKey(), context);
//空key,忽略
if (ObjectUtils.isEmpty(key)) {
continue;
}
val.put(key, resolveBody(entry.getValue(), context));
}
return val;
}
private List<Object> resolveBody(List<?> obj, Map<String, Object> context) {
List<Object> array = new ArrayList<>(obj.size());
for (Object val : obj) {
array.add(resolveBody(val, context));
}
return array;
}
}

View File

@ -0,0 +1,49 @@
package org.jetlinks.community.notify.webhook.http;
import org.jetlinks.community.notify.webhook.http.HttpWebHookTemplate;
import org.jetlinks.core.Values;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import java.util.Collections;
class HttpWebHookTemplateTest {
@Test
void testResolveBody() {
HttpWebHookTemplate template = new HttpWebHookTemplate();
template.setBody("{\"name\":\"${deviceName}\"}");
String body = template.resolveBody(Values.of(Collections.singletonMap("deviceName", "Test")));
System.out.println(body);
Assertions.assertEquals(
"{\"name\":\"Test\"}",
body);
}
@Test
void testResolveArrayBody() {
HttpWebHookTemplate template = new HttpWebHookTemplate();
template.setBody("[{\"name\":\"${deviceName}\"}]");
String body = template.resolveBody(Values.of(Collections.singletonMap("deviceName", "Test")));
System.out.println(body);
Assertions.assertEquals("[{\"name\":\"Test\"}]", body);
}
@Test
void testResolvePlainBody() {
HttpWebHookTemplate template = new HttpWebHookTemplate();
template.setBody("\"${deviceName}\"");
String body = template.resolveBody(Values.of(Collections.singletonMap("deviceName", "Test")));
System.out.println(body);
Assertions.assertEquals("\"Test\"", body);
}
}

View File

@ -18,6 +18,8 @@
<module>notify-email</module>
<module>notify-wechat</module>
<module>notify-dingtalk</module>
<module>notify-voice</module>
<module>notify-webhook</module>
</modules>

View File

@ -7,12 +7,12 @@ import org.jetlinks.core.device.DeviceRegistry;
import org.jetlinks.core.event.EventBus;
import org.jetlinks.core.things.ThingsRegistry;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
@AutoConfiguration
@Generated
public class ThingsConfiguration {

View File

@ -0,0 +1 @@
org.jetlinks.community.things.configuration.ThingsConfiguration

View File

@ -247,15 +247,16 @@ public class MenuController implements ReactiveServiceCrudController<MenuEntity,
}
@PatchMapping("/_all")
@PatchMapping("/{owner}/_all")
@SaveAction
@Transactional
@Operation(summary = "全量保存数据", description = "删除旧数据,再新增数据")
public Mono<SaveResult> saveAll(@RequestBody Flux<MenuEntity> menus) {
@Operation(summary = "保存一个应用下的全量数据", description = "应用下全部删除旧数据,再新增数据")
public Mono<SaveResult> saveOwnerAll(@PathVariable String owner, @RequestBody Flux<MenuEntity> menus) {
return this
.getService()
.createDelete()
.where(MenuEntity::getStatus, 1)
.and(MenuEntity::getOwner, owner)
.execute()
.then(
this.save(menus)

View File

@ -1,25 +1,29 @@
package org.jetlinks.community.device.configuration;
import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
import org.hswebframework.ezorm.rdb.operator.DatabaseOperator;
import org.jetlinks.community.buffer.BufferProperties;
import org.jetlinks.community.device.entity.DeviceInstanceEntity;
import org.jetlinks.community.device.function.ReactorQLDeviceSelectorBuilder;
import org.jetlinks.community.device.function.RelationDeviceSelectorProvider;
import org.jetlinks.community.device.message.DeviceMessageConnector;
import org.jetlinks.community.device.message.writer.TimeSeriesMessageWriterConnector;
import org.jetlinks.community.device.service.data.DeviceDataService;
import org.jetlinks.community.device.service.data.DeviceDataStorageProperties;
import org.jetlinks.community.device.service.data.*;
import org.jetlinks.community.rule.engine.executor.DeviceSelectorBuilder;
import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorProvider;
import org.jetlinks.core.device.DeviceRegistry;
import org.jetlinks.core.device.session.DeviceSessionManager;
import org.jetlinks.core.event.EventBus;
import org.jetlinks.core.server.MessageHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
@EnableConfigurationProperties(DeviceDataStorageProperties.class)
public class DeviceManagerConfiguration {
@ -51,4 +55,40 @@ public class DeviceManagerConfiguration {
}
@Configuration
@ConditionalOnProperty(prefix = "jetlinks.device.storage", name = "enable-last-data-in-db", havingValue = "true")
static class DeviceLatestDataServiceConfiguration {
@Bean
@ConfigurationProperties(prefix = "jetlinks.device.storage.latest.buffer")
public BufferProperties deviceLatestDataServiceBufferProperties() {
BufferProperties bufferProperties = new BufferProperties();
bufferProperties.setFilePath("./data/device-latest-data-buffer");
bufferProperties.setSize(1000);
bufferProperties.setParallelism(1);
bufferProperties.setTimeout(Duration.ofSeconds(1));
return bufferProperties;
}
@Bean(destroyMethod = "destroy")
public DatabaseDeviceLatestDataService deviceLatestDataService(DatabaseOperator databaseOperator) {
return new DatabaseDeviceLatestDataService(databaseOperator,
deviceLatestDataServiceBufferProperties());
}
}
@Bean
@ConditionalOnProperty(
prefix = "jetlinks.device.storage",
name = "enable-last-data-in-db",
havingValue = "false",
matchIfMissing = true)
@ConditionalOnMissingBean(DeviceLatestDataService.class)
public DeviceLatestDataService deviceLatestDataService() {
return new NonDeviceLatestDataService();
}
}

View File

@ -0,0 +1,34 @@
package org.jetlinks.community.device.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.HashMap;
import java.util.Map;
public class DeviceLatestData extends HashMap<String,Object> {
public DeviceLatestData(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
}
public DeviceLatestData(int initialCapacity) {
super(initialCapacity);
}
public DeviceLatestData() {
}
public DeviceLatestData(Map<? extends String, ?> m) {
super(m);
}
@Schema(description = "设备ID")
public String getDeviceId(){
return (String)get("deviceId");
}
@Schema(description = "设备名称")
public String getDeviceName(){
return (String)get("deviceName");
}
}

View File

@ -0,0 +1,111 @@
package org.jetlinks.community.device.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec;
import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec;
import org.hswebframework.web.api.crud.entity.GenericEntity;
import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
import org.hswebframework.web.crud.annotation.EnableEntityEvent;
import org.hswebframework.web.crud.generator.Generators;
import org.hswebframework.web.utils.DigestUtils;
import org.hswebframework.web.validator.CreateGroup;
import org.jetlinks.core.things.ThingMetadataType;
import org.springframework.util.StringUtils;
import javax.persistence.Column;
import javax.persistence.Index;
import javax.persistence.Table;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.sql.JDBCType;
import java.util.Map;
@Getter
@Setter
@Table(name = "dev_metadata_mapping", indexes = {
@Index(name = "idx_dev_mmp_did", columnList = "device_id"),
@Index(name = "idx_dev_mmp_pid", columnList = "product_id")
})
@Schema(description = "设备物模型映射")
@EnableEntityEvent
public class DeviceMetadataMappingEntity extends GenericEntity<String> implements RecordCreationEntity {
@Schema(description = "产品ID")
@Column(length = 64, nullable = false, updatable = false)
@NotBlank(groups = CreateGroup.class)
private String productId;
@Schema(description = "设备ID,为空时表示映射对产品下所有设备生效")
@Column(length = 64, updatable = false)
@NotBlank
private String deviceId;
@Schema(description = "物模型类型,如:property")
@Column(length = 32, nullable = false, updatable = false)
@DefaultValue("property")
@NotNull(groups = CreateGroup.class)
@EnumCodec
@ColumnType(javaType = String.class)
private ThingMetadataType metadataType;
@Schema(description = "物模型ID,如:属性ID")
@Column(length = 64, nullable = false, updatable = false)
@NotBlank(groups = CreateGroup.class)
private String metadataId;
@Schema(description = "原始物模型ID")
@Column(length = 64, nullable = false)
@NotBlank(groups = CreateGroup.class)
private String originalId;
@Schema(description = "其他配置")
@Column
@JsonCodec
@ColumnType(jdbcType = JDBCType.LONGVARCHAR)
private Map<String, Object> others;
@Schema(description = "说明")
@Column(length = 512)
private String description;
@Schema(description = "创建者ID", accessMode = Schema.AccessMode.READ_ONLY)
@Column(length = 64, updatable = false)
private String creatorId;
@Schema(description = "创建时间", accessMode = Schema.AccessMode.READ_ONLY)
@Column(updatable = false)
@DefaultValue(generator = Generators.CURRENT_TIME)
private Long createTime;
@Override
public String getId() {
if (super.getId() == null) {
generateId();
}
return super.getId();
}
public void generateId() {
if (StringUtils.hasText(deviceId)) {
setId(
generateIdByDevice(deviceId, metadataType, metadataId)
);
} else if (StringUtils.hasText(productId)) {
setId(
generateIdByProduct(productId, metadataType, metadataId)
);
}
}
public static String generateIdByProduct(String productId, ThingMetadataType type, String metadataId) {
return DigestUtils.md5Hex(String.join(":", "product", productId, type.name(), metadataId));
}
public static String generateIdByDevice(String deviceId, ThingMetadataType type, String metadataId) {
return DigestUtils.md5Hex(String.join(":", "device", deviceId, type.name(), metadataId));
}
}

View File

@ -1,10 +1,15 @@
package org.jetlinks.community.device.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec;
import org.hswebframework.web.api.crud.entity.GenericEntity;
import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
import org.hswebframework.web.crud.annotation.EnableEntityEvent;
import org.hswebframework.web.crud.generator.Generators;
import org.jetlinks.supports.protocol.management.ProtocolSupportDefinition;
import javax.persistence.Column;
@ -15,7 +20,8 @@ import java.util.Map;
@Getter
@Setter
@Table(name = "dev_protocol")
public class ProtocolSupportEntity extends GenericEntity<String> {
@EnableEntityEvent
public class ProtocolSupportEntity extends GenericEntity<String> implements RecordCreationEntity {
@Column
private String name;
@ -27,8 +33,27 @@ public class ProtocolSupportEntity extends GenericEntity<String> {
private String type;
@Column
@Schema(description = "状态1启用0禁用")
@DefaultValue("1")
private Byte state;
@Column(updatable = false)
@Schema(
description = "创建者ID(只读)"
, accessMode = Schema.AccessMode.READ_ONLY
)
private String creatorId;
@Column(updatable = false)
@DefaultValue(generator = Generators.CURRENT_TIME)
@Schema(
description = "创建时间(只读)"
, accessMode = Schema.AccessMode.READ_ONLY
)
private Long createTime;
@Column
@ColumnType(jdbcType = JDBCType.CLOB)
@JsonCodec

View File

@ -12,7 +12,7 @@ public class DeviceInstanceImportExportEntity {
@ExcelProperty("设备名称")
private String name;
@ExcelProperty("设备型号")
@ExcelProperty("产品名称")
private String productName;
@ExcelProperty("描述")

View File

@ -1,14 +1,16 @@
package org.jetlinks.community.device.enums;
import lombok.AllArgsConstructor;
import lombok.Generated;
import lombok.Getter;
import org.hswebframework.web.dict.Dict;
import org.hswebframework.web.dict.EnumDict;
import org.jetlinks.core.metadata.Feature;
@Dict("device-feature")
@Getter
@AllArgsConstructor
public enum DeviceFeature implements EnumDict<String> {
public enum DeviceFeature implements EnumDict<String> , Feature {
selfManageState("子设备自己管理状态")
@ -16,7 +18,18 @@ public enum DeviceFeature implements EnumDict<String> {
private final String text;
@Override
@Generated
public String getValue() {
return name();
}
@Override
public String getId() {
return getValue();
}
@Override
public String getName() {
return text;
}
}

View File

@ -0,0 +1,16 @@
package org.jetlinks.community.device.events;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.hswebframework.web.event.DefaultAsyncEvent;
import org.jetlinks.community.device.entity.DeviceInstanceEntity;
import java.util.List;
@Getter
@AllArgsConstructor(staticName = "of")
public class DeviceDeployedEvent extends DefaultAsyncEvent {
private final List<DeviceInstanceEntity> devices;
}

View File

@ -0,0 +1,20 @@
package org.jetlinks.community.device.events;
import lombok.AllArgsConstructor;
import lombok.Generated;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.web.event.DefaultAsyncEvent;
import org.jetlinks.community.device.entity.DeviceInstanceEntity;
import java.util.List;
@Getter
@Setter
@AllArgsConstructor(staticName = "of")
@Generated
public class DeviceUnregisterEvent extends DefaultAsyncEvent {
private List<DeviceInstanceEntity> devices;
}

View File

@ -4,6 +4,9 @@ import lombok.extern.slf4j.Slf4j;
import org.jetlinks.community.device.events.DeviceProductDeployEvent;
import org.jetlinks.community.device.service.LocalDeviceProductService;
import org.jetlinks.community.device.service.data.DeviceDataService;
import org.jetlinks.community.device.service.data.DeviceLatestDataService;
import org.jetlinks.core.event.EventBus;
import org.jetlinks.core.event.Subscription;
import org.jetlinks.core.metadata.DeviceMetadataCodec;
import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec;
import org.springframework.beans.factory.annotation.Autowired;
@ -11,8 +14,12 @@ import org.springframework.boot.CommandLineRunner;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.annotation.PreDestroy;
/**
* 处理设备产品发布事件
*
@ -31,33 +38,83 @@ public class DeviceProductDeployHandler implements CommandLineRunner {
private final DeviceDataService dataService;
private final DeviceLatestDataService latestDataService;
private final EventBus eventBus;
private final Disposable disposable;
@Autowired
public DeviceProductDeployHandler(LocalDeviceProductService productService,
DeviceDataService dataService) {
DeviceDataService dataService,
EventBus eventBus,
DeviceLatestDataService latestDataService) {
this.productService = productService;
this.dataService = dataService;
this.eventBus = eventBus;
this.latestDataService = latestDataService;
//监听其他服务器上的物模型变更
disposable = eventBus
.subscribe(Subscription
.builder()
.subscriberId("product-metadata-upgrade")
.topics("/_sys/product-upgrade")
.justBroker()
.build(), String.class)
.flatMap(id -> this
.reloadMetadata(id)
.onErrorResume((err) -> {
log.warn("handle product upgrade event error", err);
return Mono.empty();
}))
.subscribe();
}
@PreDestroy
public void shutdown() {
disposable.dispose();
}
@EventListener
public void handlerEvent(DeviceProductDeployEvent event) {
event.async(
this
.doRegisterMetadata(event.getId(), event.getMetadata())
.then(
eventBus.publish("/_sys/product-upgrade", event.getId())
)
);
}
private Mono<Void> doRegisterMetadata(String productId, String metadataString) {
protected Mono<Void> reloadMetadata(String productId) {
return productService
.findById(productId)
.flatMap(product -> doReloadMetadata(productId, product.getMetadata()))
.then();
}
protected Mono<Void> doReloadMetadata(String productId, String metadataString) {
return codec
.decode(metadataString)
.flatMap(metadata -> dataService.registerMetadata(productId, metadata));
.flatMap(metadata -> Flux
.mergeDelayError(2,
dataService.reloadMetadata(productId, metadata),
latestDataService.reloadMetadata(productId, metadata))
.then());
}
protected Mono<Void> doRegisterMetadata(String productId, String metadataString) {
return codec
.decode(metadataString)
.flatMap(metadata -> Flux
.mergeDelayError(2,
dataService.registerMetadata(productId, metadata),
latestDataService.upgradeMetadata(productId, metadata))
.then());
}
@Override
public void run(String... args) {
//启动时发布物模型
productService
.createQuery()
.fetch()
@ -65,7 +122,7 @@ public class DeviceProductDeployHandler implements CommandLineRunner {
.flatMap(deviceProductEntity -> this
.doRegisterMetadata(deviceProductEntity.getId(), deviceProductEntity.getMetadata())
.onErrorResume(err -> {
log.warn("register product metadata error", err);
log.warn("register product [{}] metadata error", deviceProductEntity.getId(), err);
return Mono.empty();
})
)

View File

@ -1,5 +1,6 @@
package org.jetlinks.community.device.measurements.message;
import lombok.Generated;
import org.jetlinks.community.Interval;
import org.jetlinks.community.dashboard.*;
import org.jetlinks.community.dashboard.supports.StaticMeasurement;
@ -33,14 +34,11 @@ class DeviceMessageMeasurement extends StaticMeasurement {
private final TimeSeriesManager timeSeriesManager;
private final DeviceRegistry deviceRegistry;
static MeasurementDefinition definition = MeasurementDefinition.of("quantity", "设备消息量");
public DeviceMessageMeasurement(EventBus eventBus,
DeviceRegistry registry,
TimeSeriesManager timeSeriesManager) {
super(definition);
this.deviceRegistry = registry;
this.eventBus = eventBus;
this.timeSeriesManager = timeSeriesManager;
addDimension(new RealTimeMessageDimension());
@ -53,21 +51,25 @@ class DeviceMessageMeasurement extends StaticMeasurement {
class RealTimeMessageDimension implements MeasurementDimension {
@Override
@Generated
public DimensionDefinition getDefinition() {
return CommonDimensionDefinition.realTime;
}
@Override
@Generated
public DataType getValueType() {
return IntType.GLOBAL;
}
@Override
@Generated
public ConfigMetadata getParams() {
return realTimeConfigMetadata;
}
@Override
@Generated
public boolean isRealTime() {
return true;
}
@ -86,7 +88,6 @@ class DeviceMessageMeasurement extends StaticMeasurement {
}
}
static ConfigMetadata historyConfigMetadata = new DefaultConfigMetadata()
.add("productId", "设备型号", "", new StringType())
.add("time", "周期", "例如: 1h,10m,30s", new StringType())
@ -100,78 +101,62 @@ class DeviceMessageMeasurement extends StaticMeasurement {
@Override
@Generated
public DimensionDefinition getDefinition() {
return CommonDimensionDefinition.agg;
}
@Override
@Generated
public DataType getValueType() {
return IntType.GLOBAL;
}
@Override
@Generated
public ConfigMetadata getParams() {
return historyConfigMetadata;
}
@Override
@Generated
public boolean isRealTime() {
return false;
}
private AggregationQueryParam createQueryParam(MeasurementParameter parameter) {
return AggregationQueryParam.of()
// .sum("count")
.groupBy(
parameter.getInterval("time").orElse(Interval.ofHours(1)),
parameter.getString("format").orElse("MM月dd日 HH时"))
// .filter(query ->
// query
// .where("name", "message-count")
// .is("productId", parameter.getString("productId").orElse(null))
// .is("msgType", parameter.getString("msgType").orElse(null))
// )
public AggregationQueryParam createQueryParam(MeasurementParameter parameter) {
return AggregationQueryParam
.of()
.sum("count")
.groupBy(parameter.getInterval("interval", parameter.getInterval("time", null)),
parameter.getString("format").orElse("MM月dd日 HH时"))
.filter(query -> query
.where("name", "message-count")
.is("productId", parameter.getString("productId").orElse(null))
)
.limit(parameter.getInt("limit").orElse(1))
.from(parameter.getDate("from").orElseGet(() -> Date.from(LocalDateTime.now().plusDays(-1).atZone(ZoneId.systemDefault()).toInstant())))
.from(parameter
.getDate("from")
.orElseGet(() -> Date
.from(LocalDateTime
.now()
.plusDays(-1)
.atZone(ZoneId.systemDefault())
.toInstant())))
.to(parameter.getDate("to").orElse(new Date()));
}
private Mono<TimeSeriesMetric[]> getProductMetrics(List<String> productIdList) {
return Flux
.fromIterable(productIdList)
.flatMap(id -> deviceRegistry
.getProduct(id)
.flatMap(DeviceProductOperator::getMetadata)
.onErrorResume(err -> Mono.empty())
.flatMapMany(metadata -> Flux.fromIterable(metadata.getEvents())
.map(event -> DeviceTimeSeriesMetric.deviceEventMetric(id, event.getId())))
.concatWithValues(DeviceTimeSeriesMetric.devicePropertyMetric(id)))
.collectList()
.map(list -> list.toArray(new TimeSeriesMetric[0]));
}
@Override
public Flux<SimpleMeasurementValue> getValue(MeasurementParameter parameter) {
AggregationQueryParam param = createQueryParam(parameter);
return AggregationQueryParam.of()
.sum("count")
.groupBy(
parameter.getInterval("time").orElse(Interval.ofHours(1)),
parameter.getString("format").orElse("MM月dd日 HH时"))
.filter(query ->
query.where("name", "message-count")
.is("productId", parameter.getString("productId").orElse(null))
.is("msgType", parameter.getString("msgType").orElse(null))
)
.limit(parameter.getInt("limit").orElse(1))
.from(parameter.getDate("from").orElseGet(() -> Date.from(LocalDateTime.now().plusDays(-1).atZone(ZoneId.systemDefault()).toInstant())))
.to(parameter.getDate("to").orElse(new Date()))
return Flux.defer(() -> param
.execute(timeSeriesManager.getService(DeviceTimeSeriesMetric.deviceMetrics())::aggregation)
.index((index, data) -> SimpleMeasurementValue.of(
data.getInt("count").orElse(0),
data.getLong("count",0),
data.getString("time").orElse(""),
index))
.sort();
index)))
.take(param.getLimit());
}
}

View File

@ -21,14 +21,13 @@ public class DeviceMessageMeasurementProvider extends StaticMeasurementProvider
public DeviceMessageMeasurementProvider(EventBus eventBus,
MeterRegistryManager registryManager,
DeviceRegistry deviceRegistry,
TimeSeriesManager timeSeriesManager) {
super(DeviceDashboardDefinition.instance, DeviceObjectDefinition.message);
registry = registryManager.getMeterRegister(DeviceTimeSeriesMetric.deviceMetrics().getId(),
"target", "msgType", "productId");
addMeasurement(new DeviceMessageMeasurement(eventBus, deviceRegistry, timeSeriesManager));
addMeasurement(new DeviceMessageMeasurement(eventBus, timeSeriesManager));
}

View File

@ -0,0 +1,107 @@
package org.jetlinks.community.device.relation;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.jetlinks.core.device.DeviceConfigKey;
import org.jetlinks.core.device.DeviceOperator;
import org.jetlinks.core.device.DeviceRegistry;
import org.jetlinks.core.message.DeviceDataManager;
import org.jetlinks.core.things.relation.ObjectType;
import org.jetlinks.core.things.relation.PropertyOperation;
import org.jetlinks.community.PropertyConstants;
import org.jetlinks.community.device.service.LocalDeviceInstanceService;
import org.jetlinks.community.relation.RelationObjectProvider;
import org.jetlinks.community.relation.impl.SimpleObjectType;
import org.jetlinks.community.relation.impl.property.PropertyOperationStrategy;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Getter
@Setter
@AllArgsConstructor
@Component
public class DeviceObjectProvider implements RelationObjectProvider {
private final DeviceDataManager deviceDataManager;
private final LocalDeviceInstanceService instanceService;
private final DeviceRegistry registry;
@Override
public String getTypeId() {
return RelationObjectProvider.TYPE_DEVICE;
}
@Override
public Mono<ObjectType> getType() {
return Mono.just(new SimpleObjectType(getTypeId(), "设备", "设备"));
}
@Override
public PropertyOperation properties(String id) {
return PropertyOperationStrategy
.composite(
PropertyOperationStrategy
.simple(registry.getDevice(id),
strategy -> strategy
.addMapper("id", DeviceOperator::getDeviceId)
.addAsyncMapper(PropertyConstants.deviceName, DeviceOperator::getSelfConfig)
.addAsyncMapper(PropertyConstants.productName, DeviceOperator::getSelfConfig)
.addAsyncMapper(PropertyConstants.productId, DeviceOperator::getSelfConfig)
.addAsyncMapper(DeviceConfigKey.deviceType, DeviceOperator::getSelfConfig)
.addAsyncMapper(DeviceConfigKey.parentGatewayId, DeviceOperator::getSelfConfig)
.addAsyncMapper(DeviceConfigKey.firstPropertyTime, DeviceOperator::getSelfConfig)),
PropertyOperationStrategy
.detect(strategy -> {
// dev@device.property.temp
// dev@device.property.temp.timestamp
strategy
.addOperation("property",
key -> getDeviceProperty(id, key))
// dev@device.tag.tagKey
.addOperation("tag",
key -> deviceDataManager
.getTags(id, key)
.map(DeviceDataManager.TagValue::getValue)
.singleOrEmpty()
)
// dev@device.config.key
.addOperation("config",
key -> registry
.getDevice(id)
.flatMap(device -> device.getConfig(key))
);
})
);
}
protected Mono<Object> getDeviceProperty(String deviceId, String property) {
if (!property.contains(".")) {
return this
.getPropertyValue(deviceId, property)
.map(DeviceDataManager.PropertyValue::getValue);
}
String[] arr = property.split("[.]");
String propertyKey = arr[0];
String valueType = arr[1];
Mono<DeviceDataManager.PropertyValue> propertyValueMono = this.getPropertyValue(deviceId, propertyKey);
if ("timestamp".equals(valueType)) {
return propertyValueMono
.map(DeviceDataManager.PropertyValue::getTimestamp);
}
if ("state".equals(valueType)) {
return propertyValueMono
.mapNotNull(DeviceDataManager.PropertyValue::getState);
}
return propertyValueMono
.map(DeviceDataManager.PropertyValue::getValue);
}
Mono<DeviceDataManager.PropertyValue> getPropertyValue(String deviceId, String property) {
return deviceDataManager.getLastProperty(deviceId, property);
}
}

View File

@ -1,9 +1,6 @@
package org.jetlinks.community.device.response;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.*;
@Getter
@Setter
@ -17,11 +14,19 @@ public class DeviceDeployResult {
private String message;
//导致错误的源头
private Object source;
//导致错误的操作
private String operation;
@Generated
public static DeviceDeployResult success(int total) {
return new DeviceDeployResult(total, true, null);
return new DeviceDeployResult(total, true, null, null, null);
}
@Generated
public static DeviceDeployResult error(String message) {
return new DeviceDeployResult(0, false, message);
return new DeviceDeployResult(0, false, message, null, null);
}
}

View File

@ -7,12 +7,19 @@ import org.apache.commons.collections4.MapUtils;
import org.jetlinks.community.device.entity.DeviceInstanceEntity;
import org.jetlinks.community.device.entity.DeviceProductEntity;
import org.jetlinks.community.device.entity.DeviceTagEntity;
import org.jetlinks.community.device.enums.DeviceFeature;
import org.jetlinks.community.device.enums.DeviceState;
import org.jetlinks.community.device.enums.DeviceType;
import org.jetlinks.community.relation.service.response.RelatedInfo;
import org.jetlinks.core.ProtocolSupport;
import org.jetlinks.core.Values;
import org.jetlinks.core.device.DeviceConfigKey;
import org.jetlinks.core.device.DeviceOperator;
import org.jetlinks.core.device.DeviceProductOperator;
import org.jetlinks.core.metadata.ConfigPropertyMetadata;
import org.jetlinks.core.metadata.DeviceMetadata;
import org.jetlinks.core.metadata.Feature;
import org.jetlinks.core.metadata.SimpleFeature;
import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
@ -126,6 +133,13 @@ public class DeviceDetail {
@Schema(description = "设备描述")
private String description;
@Schema(description = "关系信息")
private List<RelatedInfo> relations;
@Schema(description = "设备特性")
private List<Feature> features = new ArrayList<>();
public DeviceDetail notActive() {
state = DeviceState.notActive;
@ -209,6 +223,11 @@ public class DeviceDetail {
return this;
}
public DeviceDetail withRelation(List<RelatedInfo> relations){
this.relations=relations;
return this;
}
public DeviceDetail with(DeviceProductEntity productEntity) {
if (productEntity == null) {
return this;
@ -237,6 +256,9 @@ public class DeviceDetail {
setOrgId(device.getOrgId());
setParentId(device.getParentId());
setDescription(device.getDescribe());
if (device.getFeatures() != null) {
withFeatures(Arrays.asList(device.getFeatures()));
}
Optional.ofNullable(device.getRegistryTime())
.ifPresent(this::setRegisterTime);
@ -267,4 +289,32 @@ public class DeviceDetail {
return this;
}
public DeviceDetail withFeatures(Collection<? extends Feature> features) {
for (Feature feature : features) {
this.features.add(new SimpleFeature(feature.getId(), feature.getName()));
}
return this;
}
public Mono<DeviceDetail> with(DeviceProductOperator product) {
return Mono
.zip(
product
.getProtocol()
.mapNotNull(ProtocolSupport::getName)
.defaultIfEmpty(""),
product
.getConfig(DeviceConfigKey.metadata)
.defaultIfEmpty(""))
.doOnNext(tp2 -> {
setProtocolName(tp2.getT1());
//物模型以产品缓存里的为准
if (!this.independentMetadata && StringUtils.hasText(tp2.getT2())) {
setMetadata(tp2.getT2());
}
})
.thenReturn(this);
}
}

View File

@ -1,10 +1,7 @@
package org.jetlinks.community.device.service;
import org.jetlinks.community.device.spi.DeviceConfigMetadataSupplier;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.jetlinks.core.metadata.ConfigScope;
import org.jetlinks.core.metadata.DeviceConfigScope;
import org.jetlinks.core.metadata.DeviceMetadataType;
import org.jetlinks.core.metadata.*;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
@ -72,4 +69,12 @@ public class DefaultDeviceConfigMetadataManager implements DeviceConfigMetadataM
}
return bean;
}
@Override
public Flux<Feature> getProductFeatures(String productId) {
return Flux
.fromIterable(suppliers)
.flatMap(supplier -> supplier.getProductFeatures(productId))
.distinct(Feature::getId);
}
}

View File

@ -5,18 +5,21 @@ import org.hswebframework.web.exception.BusinessException;
import org.jetlinks.community.device.entity.DeviceInstanceEntity;
import org.jetlinks.community.device.entity.DeviceProductEntity;
import org.jetlinks.community.device.spi.DeviceConfigMetadataSupplier;
import org.jetlinks.core.ProtocolSupport;
import org.jetlinks.core.ProtocolSupports;
import org.jetlinks.core.message.codec.Transport;
import org.jetlinks.core.message.codec.Transports;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.jetlinks.core.metadata.DeviceConfigScope;
import org.jetlinks.core.metadata.DeviceMetadataType;
import org.jetlinks.core.metadata.Feature;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.function.BiFunction;
import java.util.function.Function;
@Component
@ -63,32 +66,16 @@ public class DefaultDeviceConfigMetadataSupplier implements DeviceConfigMetadata
.filter(metadata -> metadata.hasScope(DeviceConfigScope.product));
}
@Override
@SuppressWarnings("all")
public Flux<ConfigMetadata> getMetadataExpandsConfig(String productId,
DeviceMetadataType metadataType,
String metadataId,
String typeId) {
Assert.hasText(productId, "productId can not be empty");
Assert.notNull(metadataType, "metadataType can not be empty");
Assert.hasText(productId, "message.productId_cannot_be_empty");
Assert.notNull(metadataType, "message.metadataType_cannot_be_empty");
return productService
.createQuery()
.select(DeviceProductEntity::getMessageProtocol, DeviceProductEntity::getTransportProtocol)
.where(DeviceInstanceEntity::getId, productId)
.fetchOne()
.flatMap(product -> {
return Mono
.zip(
//消息协议
protocolSupports.getProtocol(product.getMessageProtocol()),
//传输协议
Mono.justOrEmpty(product.getTransportEnum(Transports.get())),
(protocol, transport) -> {
return protocol.getMetadataExpandsConfig(transport, metadataType, metadataId, typeId);
}
);
})
return this
.computeDeviceProtocol(productId, (protocol, transport) ->
protocol.getMetadataExpandsConfig(transport, metadataType, metadataId, typeId))
.flatMapMany(Function.identity());
}
@ -101,4 +88,35 @@ public class DefaultDeviceConfigMetadataSupplier implements DeviceConfigMetadata
.onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, product.getMessageProtocol()))
.flatMap(support -> support.getConfigMetadata(Transport.of(product.getTransportProtocol()))));
}
@Override
public Flux<Feature> getProductFeatures(String productId) {
Assert.hasText(productId, "message.productId_cannot_be_empty");
return this
.computeDeviceProtocol(productId, ProtocolSupport::getFeatures)
.flatMapMany(Function.identity());
}
@SuppressWarnings("all")
protected <T> Mono<T> computeDeviceProtocol(String productId, BiFunction<ProtocolSupport, Transport, T> computer) {
return productService
.createQuery()
.select(DeviceProductEntity::getMessageProtocol, DeviceProductEntity::getTransportProtocol)
.where(DeviceInstanceEntity::getId, productId)
.fetchOne()
.flatMap(product -> {
return Mono
.zip(
//消息协议
Mono.justOrEmpty(product.getMessageProtocol())
.flatMap(protocolSupports::getProtocol)
.onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, product.getMessageProtocol())),
//传输协议
Mono.justOrEmpty(product.getTransportProtocol())
.map(Transport::of),
computer
);
});
}
}

View File

@ -4,10 +4,7 @@ import org.jetlinks.community.device.entity.DeviceInstanceEntity;
import org.jetlinks.community.device.entity.DeviceProductEntity;
import org.jetlinks.community.device.spi.DeviceConfigMetadataSupplier;
import org.jetlinks.core.message.codec.Transport;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.jetlinks.core.metadata.ConfigPropertyMetadata;
import org.jetlinks.core.metadata.ConfigScope;
import org.jetlinks.core.metadata.DeviceMetadataType;
import org.jetlinks.core.metadata.*;
import reactor.core.publisher.Flux;
@ -78,4 +75,6 @@ public interface DeviceConfigMetadataManager {
String typeId,
ConfigScope... scopes);
Flux<Feature> getProductFeatures(String productId);
}

View File

@ -0,0 +1,133 @@
package org.jetlinks.community.device.service;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.crud.events.EntityDeletedEvent;
import org.hswebframework.web.crud.events.EntityModifyEvent;
import org.hswebframework.web.crud.events.EntityPrepareCreateEvent;
import org.hswebframework.web.crud.events.EntitySavedEvent;
import org.hswebframework.web.exception.BusinessException;
import org.jetlinks.core.ProtocolSupports;
import org.jetlinks.core.device.DeviceRegistry;
import org.jetlinks.core.message.codec.Transport;
import org.jetlinks.community.PropertyConstants;
import org.jetlinks.community.device.entity.DeviceCategoryEntity;
import org.jetlinks.community.device.entity.DeviceInstanceEntity;
import org.jetlinks.community.device.entity.DeviceProductEntity;
import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component
@AllArgsConstructor
@Slf4j
public class DeviceEntityEventHandler {
private final LocalDeviceProductService productService;
private final DeviceRegistry registry;
private final ProtocolSupports supports;
@EventListener
public void handleDeviceEvent(EntitySavedEvent<DeviceInstanceEntity> event) {
//保存设备时,自动更新注册中心里的名称
event.first(
Flux.fromIterable(event.getEntity())
.filter(device -> StringUtils.hasText(device.getName()))
.flatMap(device -> registry
.getDevice(device.getId())
.flatMap(deviceOperator -> deviceOperator.setConfig(PropertyConstants.deviceName, device.getName())))
);
}
@EventListener
public void handleDeviceEvent(EntityModifyEvent<DeviceInstanceEntity> event) {
Map<String, DeviceInstanceEntity> olds = event
.getBefore()
.stream()
.filter(device -> StringUtils.hasText(device.getId()))
.collect(Collectors.toMap(DeviceInstanceEntity::getId, Function.identity()));
//更新设备时,自动更新注册中心里的名称
event.first(
Flux.fromIterable(event.getAfter())
.filter(device -> {
DeviceInstanceEntity old = olds.get(device.getId());
return old != null && !Objects.equals(device.getName(), old.getName());
})
.flatMap(device -> registry
.getDevice(device.getId())
.flatMap(deviceOperator -> deviceOperator.setConfig(PropertyConstants.deviceName, device.getName())))
);
}
@EventListener
public void handleProductDefaultMetadata(EntityPrepareCreateEvent<DeviceProductEntity> event) {
event.async(
Flux.fromIterable(event.getEntity())
.flatMap(product -> {
//新建产品时自动填充默认物模型
if (product.getMetadata() == null &&
StringUtils.hasText(product.getMessageProtocol()) &&
StringUtils.hasText(product.getTransportProtocol())) {
return supports
.getProtocol(product.getMessageProtocol())
.flatMapMany(support -> support
.getDefaultMetadata(Transport.of(product.getTransportProtocol()))
.flatMap(JetLinksDeviceMetadataCodec.getInstance()::encode)
.doOnNext(product::setMetadata))
.onErrorResume(err -> {
log.warn("auto set product[{}] default metadata error", product.getName(), err);
return Mono.empty();
});
}
return Mono.empty();
})
);
}
@EventListener
public void handleCategoryDelete(EntityDeletedEvent<DeviceCategoryEntity> event) {
//禁止删除有产品使用的分类
event.async(
productService
.createQuery()
.in(DeviceProductEntity::getClassifiedId, event
.getEntity()
.stream()
.map(DeviceCategoryEntity::getId)
.collect(Collectors.toList()))
.count()
.doOnNext(i -> {
if (i > 0) {
throw new BusinessException("error.device_category_has_bean_use_by_product");
}
})
);
}
//修改产品分类时同步修改产品分类名称
@EventListener
public void handleCategorySave(EntitySavedEvent<DeviceCategoryEntity> event) {
event.async(
Flux.fromIterable(event.getEntity())
.flatMap(category -> productService
.createUpdate()
.set(DeviceProductEntity::getClassifiedName, category.getName())
.where(DeviceProductEntity::getClassifiedId, category.getId())
.execute()
.then())
);
}
}

View File

@ -0,0 +1,60 @@
package org.jetlinks.community.device.service;
import lombok.AllArgsConstructor;
import org.hswebframework.web.bean.FastBeanCopier;
import org.hswebframework.web.crud.events.EntityModifyEvent;
import org.hswebframework.web.crud.events.EntitySavedEvent;
import org.jetlinks.core.device.DeviceRegistry;
import org.jetlinks.community.device.entity.DeviceProductEntity;
import org.jetlinks.community.device.events.DeviceProductDeployEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* @author bestfeng
*/
@AllArgsConstructor
@Component
public class DeviceProductHandler {
private final LocalDeviceProductService productService;
private final DeviceRegistry deviceRegistry;
private final ApplicationEventPublisher eventPublisher;
@EventListener
public void handleProductSaveEvent(EntitySavedEvent<DeviceProductEntity> event) {
event.async(
applyProductConfig(event.getEntity())
);
}
@EventListener
public void handleProductSaveEvent(EntityModifyEvent<DeviceProductEntity> event) {
event.async(
applyProductConfig(event.getBefore())
);
}
//已发布状态的产品配置更新后重新应用配置
private Mono<Void> applyProductConfig(List<DeviceProductEntity> entities) {
return Flux
.fromIterable(entities)
.map(DeviceProductEntity::getId)
.as(productService::findById)
.filter(product -> product.getState() == 1)
.flatMap(product -> deviceRegistry
.register(product.toProductInfo())
.flatMap(i -> FastBeanCopier
.copy(product, new DeviceProductDeployEvent())
.publish(eventPublisher))
)
.then();
}
}

View File

@ -7,36 +7,51 @@ import org.apache.commons.collections4.MapUtils;
import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
import org.hswebframework.ezorm.rdb.mapping.ReactiveUpdate;
import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult;
import org.hswebframework.ezorm.rdb.operator.dml.Terms;
import org.hswebframework.web.api.crud.entity.PagerResult;
import org.hswebframework.web.api.crud.entity.QueryParamEntity;
import org.hswebframework.web.crud.events.EntityDeletedEvent;
import org.hswebframework.web.crud.events.EntityEventHelper;
import org.hswebframework.web.crud.service.GenericReactiveCrudService;
import org.hswebframework.web.exception.BusinessException;
import org.hswebframework.web.exception.I18nSupportException;
import org.hswebframework.web.exception.TraceSourceException;
import org.hswebframework.web.i18n.LocaleUtils;
import org.hswebframework.web.id.IDGenerator;
import org.jetlinks.community.device.entity.*;
import org.jetlinks.community.device.enums.DeviceFeature;
import org.jetlinks.community.device.enums.DeviceState;
import org.jetlinks.community.device.response.DeviceDeployResult;
import org.jetlinks.community.device.response.DeviceDetail;
import org.jetlinks.community.utils.ErrorUtils;
import org.jetlinks.core.device.DeviceConfigKey;
import org.jetlinks.core.device.DeviceOperator;
import org.jetlinks.core.device.DeviceProductOperator;
import org.jetlinks.core.device.DeviceRegistry;
import org.jetlinks.core.enums.ErrorCode;
import org.jetlinks.core.exception.DeviceOperationException;
import org.jetlinks.core.message.DeviceMessageReply;
import org.jetlinks.core.message.FunctionInvokeMessageSender;
import org.jetlinks.core.message.ReadPropertyMessageSender;
import org.jetlinks.core.message.WritePropertyMessageSender;
import org.jetlinks.core.message.codec.Transport;
import org.jetlinks.core.message.function.FunctionInvokeMessageReply;
import org.jetlinks.core.message.property.ReadPropertyMessageReply;
import org.jetlinks.core.message.property.WritePropertyMessageReply;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.jetlinks.core.metadata.PropertyMetadata;
import org.jetlinks.core.metadata.types.StringType;
import org.jetlinks.core.metadata.DeviceMetadata;
import org.jetlinks.core.metadata.MergeOption;
import org.jetlinks.core.utils.CyclicDependencyChecker;
import org.jetlinks.community.device.entity.*;
import org.jetlinks.community.device.enums.DeviceState;
import org.jetlinks.community.device.events.DeviceDeployedEvent;
import org.jetlinks.community.device.events.DeviceUnregisterEvent;
import org.jetlinks.community.device.response.DeviceDeployResult;
import org.jetlinks.community.relation.RelationObjectProvider;
import org.jetlinks.community.relation.service.RelationService;
import org.jetlinks.community.relation.service.response.RelatedInfo;
import org.jetlinks.community.utils.ErrorUtils;
import org.jetlinks.reactor.ql.utils.CastUtils;
import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec;
import org.reactivestreams.Publisher;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.reactive.TransactionalOperator;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -46,6 +61,7 @@ import reactor.util.function.Tuple3;
import reactor.util.function.Tuples;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -57,31 +73,54 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService<Devic
private final LocalDeviceProductService deviceProductService;
private final ReactiveRepository<DeviceTagEntity, String> tagRepository;
private final ApplicationEventPublisher eventPublisher;
private final DeviceConfigMetadataManager metadataManager;
@SuppressWarnings("all")
private final ReactiveRepository<DeviceTagEntity, String> tagRepository;
private final RelationService relationService;
private final TransactionalOperator transactionalOperator;
public LocalDeviceInstanceService(DeviceRegistry registry,
LocalDeviceProductService deviceProductService,
DeviceConfigMetadataManager metadataManager,
@SuppressWarnings("all")
ReactiveRepository<DeviceTagEntity, String> tagRepository) {
ReactiveRepository<DeviceTagEntity, String> tagRepository,
ApplicationEventPublisher eventPublisher,
DeviceConfigMetadataManager metadataManager,
RelationService relationService,
TransactionalOperator transactionalOperator) {
this.registry = registry;
this.deviceProductService = deviceProductService;
this.metadataManager = metadataManager;
this.tagRepository = tagRepository;
this.eventPublisher = eventPublisher;
this.metadataManager = metadataManager;
this.relationService = relationService;
this.transactionalOperator = transactionalOperator;
}
@Override
public Mono<SaveResult> save(Publisher<DeviceInstanceEntity> entityPublisher) {
return Flux.from(entityPublisher)
.doOnNext(instance -> instance.setState(null))
.as(super::save);
return Flux
.from(entityPublisher)
.flatMap(instance -> {
instance.setState(null);
if (StringUtils.isEmpty(instance.getId())) {
return handleCreateBefore(instance);
}
return registry
.getDevice(instance.getId())
.flatMap(DeviceOperator::getState)
.map(DeviceState::of)
.onErrorReturn(DeviceState.offline)
.defaultIfEmpty(DeviceState.notActive)
.doOnNext(instance::setState)
.thenReturn(instance);
})
.as(super::save);
}
/**
* 重置设备配置
*
@ -105,19 +144,24 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService<Devic
.forEach(device.getConfiguration()::remove);
}
//重置注册中心里的配置
return registry.getDevice(deviceId)
.flatMap(opts -> opts.removeConfigs(product.getConfiguration().keySet()))
.then();
return registry
.getDevice(deviceId)
.flatMap(opts -> opts.removeConfigs(product.getConfiguration().keySet()))
.then();
}
return Mono.empty();
}).then(
//更新数据库
createUpdate()
.set(device::getConfiguration)
.where(device::getId)
.execute()
})
.then(
Mono.defer(() -> {
//更新数据库
return createUpdate()
.when(device.getConfiguration() != null, update -> update.set(device::getConfiguration))
.when(device.getConfiguration() == null, update -> update.setNull(DeviceInstanceEntity::getConfiguration))
.where(device::getId)
.execute();
})
)
.thenReturn(device.getConfiguration());
.then(Mono.fromSupplier(device::getConfiguration));
})
.defaultIfEmpty(Collections.emptyMap())
;
@ -132,22 +176,48 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService<Devic
public Mono<DeviceDeployResult> deploy(String id) {
return findById(id)
.flux()
.as(this::deploy)
.as(flux -> deploy(flux, Mono::error))
.singleOrEmpty();
}
/**
* 批量发布设备到设备注册中心
* 批量发布设备到设备注册中心,并异常返回空
*
* @param flux 设备实例流
* @return 发布数量
*/
public Flux<DeviceDeployResult> deploy(Flux<DeviceInstanceEntity> flux) {
return this
.deploy(flux, err -> Mono.empty());
// .contextWrite(TraceSourceException.deepTraceContext());
}
/**
* 批量发布设备到设备注册中心并指定异常
*
* @param flux 设备实例流
* @return 发布数量
*/
public Flux<DeviceDeployResult> deploy(Flux<DeviceInstanceEntity> flux, Function<Throwable, Mono<Void>> fallback) {
//设备回滚 key: deviceId value: 操作
Map<String, Mono<Void>> rollback = new ConcurrentHashMap<>();
return flux
//添加回滚操作,用于再触发DeviceDeployedEvent事件执行失败时进行回滚.
.flatMap(device -> registry
.getDevice(device.getId())
.switchIfEmpty(Mono.fromRunnable(() -> {
//设备之前没有注册的回滚操作(注销)
rollback.put(device.getId(), registry.unregisterDevice(device.getId()));
}))
.thenReturn(device))
//发布到注册中心
.flatMap(instance -> registry
.register(instance.toDeviceInfo())
.flatMap(deviceOperator -> deviceOperator
.getState()
.checkState()//激活时检查设备状态
.onErrorReturn(org.jetlinks.core.device.DeviceState.offline)
.flatMap(r -> {
if (r.equals(org.jetlinks.core.device.DeviceState.unknown) ||
r.equals(org.jetlinks.core.device.DeviceState.noActive)) {
@ -157,12 +227,14 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService<Devic
instance.setState(DeviceState.of(r));
return Mono.just(true);
})
.flatMap(success -> success ? Mono.just(deviceOperator) : Mono.empty())
)
.thenReturn(instance))
.buffer(50)
.flatMap(success -> success ? Mono.just(deviceOperator) : Mono.empty()))
.thenReturn(instance)
//激活失败,忽略错误,继续处理其他设备
.onErrorResume(e -> fallback.apply(e).then(Mono.empty()))
)
.buffer(200)//每200条数据批量更新
.publishOn(Schedulers.single())
.flatMap(all -> Flux
.concatMap(all -> Flux
.fromIterable(all)
.groupBy(DeviceInstanceEntity::getState)
.flatMap(group -> group
@ -171,30 +243,38 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService<Devic
.flatMap(list -> createUpdate()
.where()
.set(DeviceInstanceEntity::getState, group.key())
.set(DeviceInstanceEntity::getRegistryTime, new Date())
.set(DeviceInstanceEntity::getRegistryTime, System.currentTimeMillis())
.in(DeviceInstanceEntity::getId, list)
.is(DeviceInstanceEntity::getState, DeviceState.notActive)
.execute()
.map(r -> DeviceDeployResult.success(list.size()))
.onErrorResume(err -> Mono.just(DeviceDeployResult.error(err.getMessage()))))))
.map(r -> DeviceDeployResult.success(list.size()))))
//推送激活事件
.flatMap(res -> DeviceDeployedEvent.of(all).publish(eventPublisher).thenReturn(res))
//传递国际化上下文
.as(LocaleUtils::transform)
.as(transactionalOperator::transactional)
.onErrorResume(err -> Flux
.fromIterable(all)
.mapNotNull(device -> rollback.get(device.getId()))
.flatMap(Function.identity())
.then(
Mono.zip(
I18nSupportException.tryGetLocalizedMessageReactive(err),
TraceSourceException.tryGetOperationLocalizedReactive(err).defaultIfEmpty(""),
(msg, opt) -> new DeviceDeployResult(all.size(),
false,
msg,
TraceSourceException.tryGetSource(err),
opt))
)
.flatMap(res -> fallback.apply(err).thenReturn(res))
)
)
//激活时不触发事件,单独处理DeviceDeployedEvent
.as(EntityEventHelper::setDoNotFireEvent)
;
}
/**
* 取消发布(取消激活),取消后,设备无法再连接到服务. 注册中心也无法再获取到该设备信息.
*
* @param id 设备ID
* @return 取消结果
*/
public Mono<Integer> cancelDeploy(String id) {
return findById(Mono.just(id))
.flatMap(product -> registry
.unregisterDevice(id)
.then(createUpdate()
.set(DeviceInstanceEntity::getState, DeviceState.notActive.getValue())
.where(DeviceInstanceEntity::getId, id)
.execute()));
}
/**
* 注销设备,取消后,设备无法再连接到服务. 注册中心也无法再获取到该设备信息.
*
@ -202,13 +282,9 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService<Devic
* @return 注销结果
*/
public Mono<Integer> unregisterDevice(String id) {
return this.findById(Mono.just(id))
.flatMap(device -> registry
.unregisterDevice(id)
.then(createUpdate()
.set(DeviceInstanceEntity::getState, DeviceState.notActive.getValue())
.where(DeviceInstanceEntity::getId, id)
.execute()));
return this
.unregisterDevice(Mono.just(id))
.thenReturn(1);
}
/**
@ -218,25 +294,187 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService<Devic
* @return 注销结果
*/
public Mono<Integer> unregisterDevice(Publisher<String> ids) {
return Flux.from(ids)
.flatMap(id -> registry.unregisterDevice(id).thenReturn(id))
return Flux
.from(ids)
.buffer(200)
//先修改状态
.flatMap(list -> this
.findById(list)
.collectList()
.flatMap(devices -> DeviceUnregisterEvent.of(devices).publish(eventPublisher))
.then(this
.createUpdate()
.set(DeviceInstanceEntity::getState, DeviceState.notActive.getValue())
.where().in(DeviceInstanceEntity::getId, list)
.execute()
.thenReturn(list)
))
.flatMapIterable(Function.identity())
//再注销
.flatMap(id -> registry
.getDevice(id)
.flatMap(DeviceOperator::disconnect)
.onErrorResume(err -> Mono.empty())
.then(registry.unregisterDevice(id))
.onErrorResume(err -> Mono.empty())
.thenReturn(id))
.count()
.map(Long::intValue)
//注销不触发事件,单独处理DeviceDeployedEvent
.as(EntityEventHelper::setDoNotFireEvent);
}
@Override
public Mono<Integer> deleteById(Publisher<String> idPublisher) {
return Flux.from(idPublisher)
.collectList()
.flatMap(list -> createUpdate()
.set(DeviceInstanceEntity::getState, DeviceState.notActive.getValue())
.where().in(DeviceInstanceEntity::getId, list)
.flatMap(list -> createDelete()
.where()
.in(DeviceInstanceEntity::getId, list)
.and(DeviceInstanceEntity::getState, DeviceState.notActive)
.execute());
}
protected Mono<DeviceDetail> createDeviceDetail(DeviceProductEntity product,
DeviceInstanceEntity device,
List<DeviceTagEntity> tags) {
private boolean hasContext(QueryParamEntity param, String key) {
return param
.getContext(key)
.map(CastUtils::castBoolean)
.orElse(true);
}
//分页查询设备详情列表
public Mono<PagerResult<DeviceDetail>> queryDeviceDetail(QueryParamEntity entity) {
return this
.queryPager(entity)
.filter(e -> CollectionUtils.isNotEmpty(e.getData()))
.flatMap(result -> this
.convertDeviceInstanceToDetail(result.getData(),
hasContext(entity, "includeTags"),
hasContext(entity, "includeBind"),
hasContext(entity, "includeRelations"),
hasContext(entity, "includeFirmwareInfos"))
.collectList()
.map(detailList -> PagerResult.of(result.getTotal(), detailList, entity)))
.defaultIfEmpty(PagerResult.empty());
}
//查询设备详情列表
public Flux<DeviceDetail> queryDeviceDetailList(QueryParamEntity entity) {
return this
.query(entity)
.collectList()
.flatMapMany(list -> this
.convertDeviceInstanceToDetail(list,
hasContext(entity, "includeTags"),
hasContext(entity, "includeBind"),
hasContext(entity, "includeRelations"),
hasContext(entity, "includeFirmwareInfos")));
}
private Mono<Map<String, List<DeviceTagEntity>>> queryDeviceTagGroup(Collection<String> deviceIdList) {
return tagRepository
.createQuery()
.where()
.in(DeviceTagEntity::getDeviceId, deviceIdList)
.fetch()
.collect(Collectors.groupingBy(DeviceTagEntity::getDeviceId))
.defaultIfEmpty(Collections.emptyMap());
}
private Flux<DeviceDetail> convertDeviceInstanceToDetail(List<DeviceInstanceEntity> instanceList,
boolean includeTag,
boolean includeBinds,
boolean includeRelations,
boolean includeFirmwareInfos) {
if (CollectionUtils.isEmpty(instanceList)) {
return Flux.empty();
}
List<String> deviceIdList = new ArrayList<>(instanceList.size());
//按设备产品分组
Map<String, List<DeviceInstanceEntity>> productGroup = instanceList
.stream()
.peek(device -> deviceIdList.add(device.getId()))
.collect(Collectors.groupingBy(DeviceInstanceEntity::getProductId));
//标签
Mono<Map<String, List<DeviceTagEntity>>> tags = includeTag
? this.queryDeviceTagGroup(deviceIdList)
: Mono.just(Collections.emptyMap());
//关系信息
Mono<Map<String, List<RelatedInfo>>> relations = includeRelations ? relationService
.getRelationInfo(RelationObjectProvider.TYPE_DEVICE, deviceIdList)
.collect(Collectors.groupingBy(RelatedInfo::getObjectId))
.defaultIfEmpty(Collections.emptyMap())
: Mono.just(Collections.emptyMap());
DeviceDetail detail = new DeviceDetail().with(product).with(device).with(tags);
return Mono
.zip(
//T1:查询出所有设备的产品信息
deviceProductService
.findById(productGroup.keySet())
.collect(Collectors.toMap(DeviceProductEntity::getId, Function.identity())),
//T2:查询出标签并按设备ID分组
tags,
//T3: 关系信息
relations
)
.flatMapMany(tp5 -> Flux
//遍历设备,将设备信息转为详情.
.fromIterable(instanceList)
.flatMap(instance -> this
.createDeviceDetail(
// 设备
instance
//产品
, tp5.getT1().get(instance.getProductId())
//标签
, tp5.getT2().get(instance.getId())
//关系信息
, tp5.getT3().get(instance.getId())
)
))
//createDeviceDetail是异步操作,可能导致顺序错乱.进行重新排序.
.sort(Comparator.comparingInt(detail -> deviceIdList.indexOf(detail.getId())))
;
}
private Mono<DeviceDetail> createDeviceDetail(DeviceInstanceEntity device,
DeviceProductEntity product,
List<DeviceTagEntity> tags,
List<RelatedInfo> relations) {
if (product == null) {
log.warn("device [{}] product [{}] does not exists", device.getId(), device.getProductId());
return Mono.empty();
}
DeviceDetail detail = new DeviceDetail()
.with(product)
.with(device)
.with(tags)
.withRelation(relations);
return Mono
.zip(
//产品注册信息
registry
.getProduct(product.getId()),
//feature信息
metadataManager
.getProductFeatures(product.getId())
.collectList())
.flatMap(t2 -> {
//填充产品中feature信息
detail.withFeatures(t2.getT2());
//填充注册中心里的产品信息
return detail.with(t2.getT1());
})
.then(Mono.zip(
//设备信息
registry
.getDevice(device.getId())
//先刷新配置缓存
.flatMap(operator -> operator.refreshAllConfig().thenReturn(operator))
.flatMap(operator -> operator
//检查设备的真实状态,可能出现设备已经离线,但是数据库状态未及时更新的.
.checkState()
@ -255,11 +493,11 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService<Devic
metadataManager
.getDeviceConfigMetadata(device.getId())
.flatMapIterable(ConfigMetadata::getProperties)
.collectList(),
detail::with
)
.collectList()
))
//填充详情信息
.flatMap(Function.identity())
.flatMap(tp2 -> detail
.with(tp2.getT1(), tp2.getT2()))
.switchIfEmpty(
Mono.defer(() -> {
//如果设备注册中心里没有设备信息,并且数据库里的状态不是未激活.
@ -277,39 +515,168 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService<Devic
log.warn("get device detail error", err);
return Mono.just(detail);
});
}
public Mono<DeviceDetail> getDeviceDetail(String deviceId) {
return this
.findById(deviceId)
.zipWhen(device -> deviceProductService.findById(device.getProductId()))//合并型号
.zipWith(tagRepository
.createQuery()
.where(DeviceTagEntity::getDeviceId, deviceId)
.fetch()
.collectList()
.defaultIfEmpty(Collections.emptyList()) //合并标签
, (left, right) -> Tuples.of(left.getT2(), left.getT1(), right))
.flatMap(tp3 -> createDeviceDetail(tp3.getT1(), tp3.getT2(), tp3.getT3()));
.map(Collections::singletonList)
.flatMapMany(list -> convertDeviceInstanceToDetail(list, true, true, true, true))
.next();
}
public Mono<DeviceState> getDeviceState(String deviceId) {
return registry
.getDevice(deviceId)
.flatMap(DeviceOperator::checkState)
.flatMap(state -> {
DeviceState deviceState = DeviceState.of(state);
return this
.createUpdate()
.set(DeviceInstanceEntity::getState, deviceState)
.where(DeviceInstanceEntity::getId, deviceId)
.execute()
.thenReturn(deviceState);
})
.defaultIfEmpty(DeviceState.notActive);
return registry.getDevice(deviceId)
.flatMap(DeviceOperator::checkState)
.flatMap(state -> {
DeviceState deviceState = DeviceState.of(state);
return createUpdate()
.set(DeviceInstanceEntity::getState, deviceState)
.where(DeviceInstanceEntity::getId, deviceId)
.execute()
.thenReturn(deviceState);
})
.defaultIfEmpty(DeviceState.notActive);
}
//获取设备属性
@SneakyThrows
public Mono<Map<String, Object>> readProperty(String deviceId,
String property) {
return registry
.getDevice(deviceId)
.switchIfEmpty(ErrorUtils.notFound("error.device_not_found_or_not_activated"))
.map(DeviceOperator::messageSender)//发送消息到设备
.map(sender -> sender.readProperty(property).messageId(IDGenerator.SNOW_FLAKE_STRING.generate()))
.flatMapMany(ReadPropertyMessageSender::send)
.flatMap(mapReply(ReadPropertyMessageReply::getProperties))
.reduceWith(LinkedHashMap::new, (main, map) -> {
main.putAll(map);
return main;
});
}
//获取标准设备属性
@SneakyThrows
public Mono<DeviceProperty> readAndConvertProperty(String deviceId,
String property) {
return registry
.getDevice(deviceId)
.switchIfEmpty(ErrorUtils.notFound("error.device_not_found_or_not_activated"))
.flatMap(deviceOperator -> deviceOperator
.messageSender()
.readProperty(property)
.messageId(IDGenerator.SNOW_FLAKE_STRING.generate())
.send()
.flatMap(mapReply(ReadPropertyMessageReply::getProperties))
.reduceWith(LinkedHashMap::new, (main, map) -> {
main.putAll(map);
return main;
})
.flatMap(map -> {
Object value = map.get(property);
return deviceOperator
.getMetadata()
.map(deviceMetadata -> DeviceProperty.of(value, deviceMetadata.getPropertyOrNull(property)));
}));
}
//设置设备属性
@SneakyThrows
public Mono<Map<String, Object>> writeProperties(String deviceId,
Map<String, Object> properties) {
return registry
.getDevice(deviceId)
.switchIfEmpty(ErrorUtils.notFound("error.device_not_found_or_not_activated"))
.flatMap(operator -> operator
.messageSender()
.writeProperty()
.messageId(IDGenerator.SNOW_FLAKE_STRING.generate())
.write(properties)
.validate()
)
.flatMapMany(WritePropertyMessageSender::send)
.flatMap(mapReply(WritePropertyMessageReply::getProperties))
.reduceWith(LinkedHashMap::new, (main, map) -> {
main.putAll(map);
return main;
});
}
//设备功能调用
@SneakyThrows
public Flux<?> invokeFunction(String deviceId,
String functionId,
Map<String, Object> properties) {
return invokeFunction(deviceId, functionId, properties, true);
}
//设备功能调用
@SneakyThrows
public Flux<?> invokeFunction(String deviceId,
String functionId,
Map<String, Object> properties,
boolean convertReply) {
return registry
.getDevice(deviceId)
.switchIfEmpty(ErrorUtils.notFound("error.device_not_found_or_not_activated"))
.flatMap(operator -> operator
.messageSender()
.invokeFunction(functionId)
.messageId(IDGenerator.SNOW_FLAKE_STRING.generate())
.setParameter(properties)
.validate()
)
.flatMapMany(FunctionInvokeMessageSender::send)
.flatMap(convertReply ? mapReply(FunctionInvokeMessageReply::getOutput) : Mono::just);
}
//获取设备所有属性
@SneakyThrows
public Mono<Map<String, Object>> readProperties(String deviceId, List<String> properties) {
return registry.getDevice(deviceId)
.switchIfEmpty(ErrorUtils.notFound("error.device_not_found_or_not_activated"))
.map(DeviceOperator::messageSender)
.flatMapMany((sender) -> sender.readProperty()
.read(properties)
.messageId(IDGenerator.SNOW_FLAKE_STRING.generate())
.send())
.flatMap(mapReply(ReadPropertyMessageReply::getProperties))
.reduceWith(LinkedHashMap::new, (main, map) -> {
main.putAll(map);
return main;
});
}
private static <R extends DeviceMessageReply, T> Function<R, Mono<T>> mapReply(Function<R, T> function) {
return reply -> {
if (ErrorCode.REQUEST_HANDLING.name().equals(reply.getCode())) {
throw new DeviceOperationException(ErrorCode.REQUEST_HANDLING, reply.getMessage());
}
if (!reply.isSuccess()) {
if (StringUtils.isEmpty(reply.getMessage())) {
throw new BusinessException("error.reply_is_error");
}
throw new BusinessException(reply.getMessage(), reply.getCode());
}
return Mono.justOrEmpty(function.apply(reply));
};
}
/**
* 批量同步设备状态
*
* @param batch 设备状态ID流
* @param force 是否强制获取设备状态,强制获取会去设备连接到服务器检查设备是否真实在线
* @return 同步数量
*/
public Flux<List<DeviceStateInfo>> syncStateBatch(Flux<List<String>> batch, boolean force) {
return batch
@ -363,92 +730,108 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService<Devic
.as(EntityEventHelper::setDoNotFireEvent);
}
private static <R extends DeviceMessageReply, T> Function<R, Mono<T>> mapReply(Function<R, T> function) {
return reply -> {
if (ErrorCode.REQUEST_HANDLING.name().equals(reply.getCode())) {
throw new DeviceOperationException(ErrorCode.REQUEST_HANDLING, reply.getMessage());
}
if (!reply.isSuccess()) {
throw new BusinessException(reply.getMessage(), reply.getCode());
}
return Mono.justOrEmpty(function.apply(reply));
};
}
//获取标准设备属性
@SneakyThrows
public Mono<DevicePropertiesEntity> readAndConvertProperty(String deviceId,
String property) {
return registry
.getDevice(deviceId)
.switchIfEmpty(ErrorUtils.notFound("设备不存在"))
.flatMap(deviceOperator -> deviceOperator
.messageSender()
.readProperty(property)
.messageId(IDGenerator.SNOW_FLAKE_STRING.generate())
.send()
.flatMap(mapReply(ReadPropertyMessageReply::getProperties))
.reduceWith(LinkedHashMap::new, (main, map) -> {
main.putAll(map);
return main;
})
.flatMap(map -> {
Object value = map.get(property);
return deviceOperator
.getMetadata()
.map(deviceMetadata -> deviceMetadata
.getProperty(property)
.map(PropertyMetadata::getValueType)
.orElse(new StringType()))
.map(dataType -> DevicePropertiesEntity
.builder()
.deviceId(deviceId)
.productId(property)
.build()
.withValue(dataType, value));
}));
public Mono<Void> mergeMetadata(String deviceId, DeviceMetadata metadata, MergeOption... options) {
}
//设置设备属性
@SneakyThrows
public Mono<Map<String, Object>> writeProperties(String deviceId,
Map<String, Object> properties) {
return registry
.getDevice(deviceId)
.switchIfEmpty(ErrorUtils.notFound("设备不存在"))
.map(operator -> operator
.messageSender()
.writeProperty()
.messageId(IDGenerator.SNOW_FLAKE_STRING.generate())
.write(properties)
return Mono
.zip(this.findById(deviceId)
.flatMap(device -> {
if (StringUtils.hasText(device.getDeriveMetadata())) {
return Mono.just(device.getDeriveMetadata());
} else {
return deviceProductService
.findById(device.getProductId())
.map(DeviceProductEntity::getMetadata);
}
})
.flatMap(JetLinksDeviceMetadataCodec.getInstance()::decode),
Mono.just(metadata),
(older, newer) -> older.merge(newer, options)
)
.flatMapMany(WritePropertyMessageSender::send)
.flatMap(mapReply(WritePropertyMessageReply::getProperties))
.reduceWith(LinkedHashMap::new, (main, map) -> {
main.putAll(map);
return main;
});
.flatMap(JetLinksDeviceMetadataCodec.getInstance()::encode)
.flatMap(newMetadata -> createUpdate()
.set(DeviceInstanceEntity::getDeriveMetadata, newMetadata)
.where(DeviceInstanceEntity::getId, deviceId)
.execute()
.then(
registry
.getDevice(deviceId)
.flatMap(device -> device.updateMetadata(newMetadata))
))
.then();
}
//设备功能调用
@SneakyThrows
public Flux<?> invokeFunction(String deviceId,
String functionId,
Map<String, Object> properties) {
return registry
.getDevice(deviceId)
.switchIfEmpty(ErrorUtils.notFound("设备不存在"))
.flatMap(operator -> operator
.messageSender()
.invokeFunction(functionId)
.messageId(IDGenerator.SNOW_FLAKE_STRING.generate())
.setParameter(properties)
.validate()
public Flux<DeviceTagEntity> queryDeviceTag(String deviceId, String... tags) {
return tagRepository
.createQuery()
.where(DeviceTagEntity::getDeviceId, deviceId)
.when(tags.length > 0, q -> q.in(DeviceTagEntity::getKey, Arrays.asList(tags)))
.fetch();
}
//删除设备时删除设备标签
@EventListener
public void handleDeviceDelete(EntityDeletedEvent<DeviceInstanceEntity> event) {
event.async(
Flux.concat(
Flux
.fromIterable(event.getEntity())
.flatMap(device -> registry
.unregisterDevice(device.getId())
.onErrorResume(err -> Mono.empty())
)
.then(),
tagRepository
.createDelete()
.where()
.in(DeviceTagEntity::getDeviceId, event
.getEntity()
.stream()
.map(DeviceInstanceEntity::getId)
.collect(Collectors.toSet()))
.execute()
)
.flatMapMany(FunctionInvokeMessageSender::send)
.flatMap(mapReply(FunctionInvokeMessageReply::getOutput));
);
}
@Override
public Mono<Integer> insert(DeviceInstanceEntity data) {
return this
.handleCreateBefore(data)
.flatMap(super::insert);
}
@Override
public Mono<Integer> insert(Publisher<DeviceInstanceEntity> entityPublisher) {
return super.insert(Flux.from(entityPublisher).flatMap(this::handleCreateBefore));
}
@Override
public Mono<Integer> insertBatch(Publisher<? extends Collection<DeviceInstanceEntity>> entityPublisher) {
return Flux.from(entityPublisher)
.flatMapIterable(Function.identity())
.as(this::insert);
}
private Mono<DeviceInstanceEntity> handleCreateBefore(DeviceInstanceEntity instanceEntity) {
return Mono
.zip(
deviceProductService.findById(instanceEntity.getProductId()),
registry
.getProduct(instanceEntity.getProductId())
.flatMap(DeviceProductOperator::getProtocol),
(product, protocol) -> protocol.doBeforeDeviceCreate(Transport.of(product.getTransportProtocol()), instanceEntity
.toDeviceInfo())
)
.flatMap(Function.identity())
.doOnNext(info -> {
if (StringUtils.isEmpty(instanceEntity.getId())) {
instanceEntity.setId(info.getId());
}
instanceEntity.mergeConfiguration(info.getConfiguration());
})
.thenReturn(instanceEntity);
}
private final CyclicDependencyChecker<DeviceInstanceEntity, Void> checker = CyclicDependencyChecker
@ -495,34 +878,4 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService<Devic
}
/**
* 删除设备后置处理,解绑子设备和网关,并在注册中心取消激活已激活设备.
*/
private Flux<Void> deletedHandle(Flux<DeviceInstanceEntity> devices) {
return devices.filter(device -> !StringUtils.isEmpty(device.getParentId()))
.groupBy(DeviceInstanceEntity::getParentId)
.flatMap(group -> {
String parentId = group.key();
return group.flatMap(child -> registry.getDevice(child.getId())
.flatMap(device -> device.removeConfig(DeviceConfigKey.parentGatewayId.getKey()).thenReturn(device))
)
.as(childrenDeviceOp -> registry.getDevice(parentId)
.flatMap(gwOperator -> gwOperator.getProtocol()
.flatMap(protocolSupport -> protocolSupport.onChildUnbind(gwOperator, childrenDeviceOp))
)
);
})
// 取消激活
.thenMany(
devices.filter(device -> device.getState() != DeviceState.notActive)
.flatMap(device -> registry.unregisterDevice(device.getId()))
);
}
@EventListener
public void deletedHandle(EntityDeletedEvent<DeviceInstanceEntity> event) {
event.async(
this.deletedHandle(Flux.fromIterable(event.getEntity())).then()
);
}
}

View File

@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j;
import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
import org.hswebframework.web.bean.FastBeanCopier;
import org.hswebframework.web.crud.service.GenericReactiveCrudService;
import org.hswebframework.web.exception.BusinessException;
import org.jetlinks.community.device.entity.DeviceInstanceEntity;
import org.jetlinks.community.device.entity.DeviceProductEntity;
import org.jetlinks.community.device.enums.DeviceProductState;
@ -13,6 +14,7 @@ import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -29,10 +31,13 @@ public class LocalDeviceProductService extends GenericReactiveCrudService<Device
@Autowired
private ReactiveRepository<DeviceInstanceEntity, String> instanceRepository;
public Mono<Integer> deploy(String id) {
return findById(Mono.just(id))
.doOnNext(this::validateDeviceProduct)
.flatMap(product -> registry
.register(product.toProductInfo())
.onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, product.getMessageProtocol()))
.then(
createUpdate()
.set(DeviceProductEntity::getState, DeviceProductState.registered.getValue())
@ -46,6 +51,13 @@ public class LocalDeviceProductService extends GenericReactiveCrudService<Device
);
}
private void validateDeviceProduct(DeviceProductEntity product) {
// 设备接入ID不能为空
// Assert.hasText(product.getAccessId(), "error.access_id_can_not_be_empty");
// 发布前必须填写消息协议
Assert.hasText(product.getMessageProtocol(), "error.please_select_the_access_mode_first");
}
public Mono<Integer> cancelDeploy(String id) {
return createUpdate()

View File

@ -0,0 +1,651 @@
package org.jetlinks.community.device.service.data;
import com.google.common.collect.Maps;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.ezorm.core.ValueCodec;
import org.hswebframework.ezorm.rdb.codec.ClobValueCodec;
import org.hswebframework.ezorm.rdb.codec.DateTimeCodec;
import org.hswebframework.ezorm.rdb.codec.JsonValueCodec;
import org.hswebframework.ezorm.rdb.codec.NumberValueCodec;
import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrappers;
import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
import org.hswebframework.ezorm.rdb.mapping.defaults.record.Record;
import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata;
import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata;
import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata;
import org.hswebframework.ezorm.rdb.operator.DatabaseOperator;
import org.hswebframework.ezorm.rdb.operator.ddl.TableBuilder;
import org.hswebframework.ezorm.rdb.operator.dml.SelectColumnSupplier;
import org.hswebframework.ezorm.rdb.operator.dml.query.Selects;
import org.hswebframework.web.api.crud.entity.QueryParamEntity;
import org.hswebframework.web.exception.ValidationException;
import org.jetlinks.core.event.Subscription;
import org.jetlinks.core.message.DeviceMessage;
import org.jetlinks.core.message.event.EventMessage;
import org.jetlinks.core.metadata.DataType;
import org.jetlinks.core.metadata.DeviceMetadata;
import org.jetlinks.core.metadata.EventMetadata;
import org.jetlinks.core.metadata.PropertyMetadata;
import org.jetlinks.core.metadata.types.*;
import org.jetlinks.core.utils.Reactors;
import org.jetlinks.core.utils.SerializeUtils;
import org.jetlinks.core.utils.StringBuilderUtils;
import org.jetlinks.community.ConfigMetadataConstants;
import org.jetlinks.community.buffer.BufferProperties;
import org.jetlinks.community.buffer.BufferSettings;
import org.jetlinks.community.buffer.PersistenceBuffer;
import org.jetlinks.community.device.entity.DeviceLatestData;
import org.jetlinks.community.gateway.DeviceMessageUtils;
import org.jetlinks.community.gateway.annotation.Subscribe;
import org.jetlinks.community.timeseries.query.Aggregation;
import org.jetlinks.community.timeseries.query.AggregationColumn;
import org.jetlinks.reactor.ql.utils.CastUtils;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.math.MathFlux;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.sql.JDBCType;
import java.time.Duration;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 设备最新数据服务,用于保存设备最新的相关数据到关系型数据库中可以使用动态条件进行查询相关数据
*
* @author zhouhao
* @since 1.5.0
*/
@Slf4j
public class DatabaseDeviceLatestDataService implements DeviceLatestDataService {
private final DatabaseOperator databaseOperator;
private final BufferProperties buffer;
private PersistenceBuffer<Buffer> writer;
public DatabaseDeviceLatestDataService(DatabaseOperator databaseOperator, BufferProperties properties) {
this.databaseOperator = databaseOperator;
this.buffer = properties;
init();
}
public static String getLatestTableTableName(String productId) {
return StringBuilderUtils.buildString(productId, (p, b) -> {
b.append("dev_lst_");
for (char c : productId.toCharArray()) {
if (c == '-' || c == '.') {
b.append('_');
} else {
b.append(Character.toLowerCase(c));
}
}
});
}
private String getEventColumn(String event, String property) {
return event + "_" + property;
}
private Mono<Boolean> doWrite(Flux<Buffer> flux) {
return flux
.groupBy(Buffer::getTable, Integer.MAX_VALUE)
.concatMap(group -> group
.groupBy(Buffer::getDeviceId, Integer.MAX_VALUE)
.flatMap(sameDevice -> sameDevice.reduce(Buffer::merge))
.buffer(200)
//批量更新
.flatMap(sameTableData -> {
Buffer first = sameTableData.get(0);
List<Map<String, Object>> data = sameTableData
.stream()
.map(Buffer::getProperties)
.collect(Collectors.toList());
return this
.doUpdateLatestData(first.table, data)
.onErrorResume((err) -> {
log.error("save device latest data error", err);
return Mono.empty();
});
}))
.then(Reactors.ALWAYS_FALSE);
}
public void init() {
writer = new PersistenceBuffer<>(
BufferSettings.create("./data/buffer", buffer),
Buffer::new,
this::doWrite)
.name("device-latest-data")
//最大缓冲10万条数据
.settings(setting -> setting.bufferSize(10_0000));
writer.start();
}
public void destroy() {
writer.dispose();
}
static GeoCodec geoCodec = new GeoCodec();
static StringCodec stringCodec = new StringCodec();
static class GeoCodec implements ValueCodec<String, GeoPoint> {
@Override
public String encode(Object value) {
return String.valueOf(value);
}
@Override
public GeoPoint decode(Object data) {
return GeoPoint.of(data);
}
}
static class StringCodec implements ValueCodec<String, String> {
@Override
public String encode(Object value) {
return String.valueOf(value);
}
@Override
public String decode(Object data) {
return String.valueOf(data);
}
}
Class<?> getJavaType(DataType dataType) {
if (null == dataType) {
return Map.class;
}
switch (dataType.getType()) {
case IntType.ID:
return Integer.class;
case LongType.ID:
return Long.class;
case FloatType.ID:
return Float.class;
case DoubleType.ID:
return Double.class;
case BooleanType.ID:
return Boolean.class;
case DateTimeType.ID:
return Date.class;
case ArrayType.ID:
return List.class;
case GeoType.ID:
case ObjectType.ID:
return Map.class;
default:
return String.class;
}
}
RDBColumnMetadata convertColumn(PropertyMetadata metadata) {
RDBColumnMetadata column = new RDBColumnMetadata();
column.setName(metadata.getId());
column.setComment(metadata.getName());
DataType type = metadata.getValueType();
if (type instanceof NumberType) {
column.setLength(32);
column.setPrecision(32);
if (type instanceof DoubleType) {
column.setScale(Optional.ofNullable(((DoubleType) type).getScale()).orElse(2));
column.setValueCodec(new NumberValueCodec(Double.class));
column.setJdbcType(JDBCType.NUMERIC, Double.class);
} else if (type instanceof FloatType) {
column.setScale(Optional.ofNullable(((FloatType) type).getScale()).orElse(2));
column.setValueCodec(new NumberValueCodec(Float.class));
column.setJdbcType(JDBCType.NUMERIC, Float.class);
} else if (type instanceof LongType) {
column.setValueCodec(new NumberValueCodec(Long.class));
column.setJdbcType(JDBCType.NUMERIC, Long.class);
} else {
column.setValueCodec(new NumberValueCodec(IntType.class));
column.setJdbcType(JDBCType.NUMERIC, Integer.class);
}
} else if (type instanceof ObjectType) {
column.setJdbcType(JDBCType.CLOB, String.class);
column.setValueCodec(JsonValueCodec.of(Map.class));
} else if (type instanceof ArrayType) {
column.setJdbcType(JDBCType.CLOB, String.class);
ArrayType arrayType = ((ArrayType) type);
column.setValueCodec(JsonValueCodec.ofCollection(ArrayList.class, getJavaType(arrayType.getElementType())));
} else if (type instanceof DateTimeType) {
column.setJdbcType(JDBCType.TIMESTAMP, Long.class);
String format = ((DateTimeType) type).getFormat();
if (DateTimeType.TIMESTAMP_FORMAT.equals(format)) {
format = "yyyy-MM-dd HH:mm:ss";
}
column.setValueCodec(new DateTimeCodec(format, Long.class));
} else if (type instanceof GeoType) {
column.setJdbcType(JDBCType.VARCHAR, String.class);
column.setValueCodec(geoCodec);
column.setLength(128);
} else if (type instanceof EnumType) {
column.setJdbcType(JDBCType.VARCHAR, String.class);
column.setValueCodec(stringCodec);
column.setLength(64);
} else {
int len = type
.getExpand(ConfigMetadataConstants.maxLength.getKey())
.filter(o -> !StringUtils.isEmpty(o))
.map(CastUtils::castNumber)
.map(Number::intValue)
.orElse(255);
if (len > 2048) {
column.setJdbcType(JDBCType.LONGVARBINARY, String.class);
column.setValueCodec(ClobValueCodec.INSTANCE);
} else {
column.setJdbcType(JDBCType.VARCHAR, String.class);
column.setLength(len);
column.setValueCodec(stringCodec);
}
}
return column;
}
public Mono<Void> reloadMetadata(String productId, DeviceMetadata metadata) {
return Mono
.defer(() -> {
String tableName = getLatestTableTableName(productId);
log.debug("reload product[{}] metadata,table name:[{}] ", productId, tableName);
RDBSchemaMetadata schema = databaseOperator.getMetadata()
.getCurrentSchema();
RDBTableMetadata table = schema.newTable(tableName);
RDBColumnMetadata id = table.newColumn();
id.setName("id");
id.setLength(64);
id.setPrimaryKey(true);
id.setJdbcType(JDBCType.VARCHAR, String.class);
table.addColumn(id);
RDBColumnMetadata deviceName = table.newColumn();
deviceName.setLength(128);
deviceName.setName("device_name");
deviceName.setAlias("deviceName");
deviceName.setJdbcType(JDBCType.VARCHAR, String.class);
table.addColumn(deviceName);
for (PropertyMetadata property : metadata.getProperties()) {
table.addColumn(convertColumn(property));
}
for (EventMetadata event : metadata.getEvents()) {
DataType type = event.getType();
if (type instanceof ObjectType) {
for (PropertyMetadata property : ((ObjectType) type).getProperties()) {
RDBColumnMetadata column = convertColumn(property);
column.setName(getEventColumn(event.getId(), property.getId()));
table.addColumn(column);
}
}
}
return schema
.getTableReactive(tableName, false)
.doOnNext(oldTable -> oldTable.replace(table))
.switchIfEmpty(Mono.fromRunnable(() -> schema.addTable(table)))
.then();
});
}
@Transactional(propagation = Propagation.NEVER)
public Mono<Void> upgradeMetadata(String productId, DeviceMetadata metadata, boolean ddl) {
return Mono
.defer(() -> {
String tableName = getLatestTableTableName(productId);
log.debug("upgrade product[{}] metadata,table name:[{}] ", productId, tableName);
TableBuilder builder = databaseOperator
.ddl()
.createOrAlter(tableName)
.addColumn("id").primaryKey().varchar(64).commit()
.addColumn("device_name").alias("deviceName").varchar(128).notNull().commit()
.merge(true)
.allowAlter(ddl);
for (PropertyMetadata property : metadata.getProperties()) {
builder.addColumn(convertColumn(property));
}
for (EventMetadata event : metadata.getEvents()) {
DataType type = event.getType();
if (type instanceof ObjectType) {
for (PropertyMetadata property : ((ObjectType) type).getProperties()) {
RDBColumnMetadata column = convertColumn(property);
column.setName(getEventColumn(event.getId(), property.getId()));
builder.addColumn(column);
}
}
}
return builder
.commit()
.reactive()
.subscribeOn(Schedulers.boundedElastic())
.then();
});
}
public Mono<Void> upgradeMetadata(String productId, DeviceMetadata metadata) {
return upgradeMetadata(productId, metadata, true);
}
@Subscribe(topics = "/device/**", features = Subscription.Feature.local)
public void save(DeviceMessage message) {
try {
Map<String, Object> properties = DeviceMessageUtils
.tryGetProperties(message)
.orElseGet(() -> {
//事件
if (message instanceof EventMessage) {
Object data = ((EventMessage) message).getData();
String event = ((EventMessage) message).getEvent();
if (data instanceof Map) {
Map<?, ?> mapValue = (Map<?, ?>) data;
Map<String, Object> val = Maps.newHashMapWithExpectedSize(mapValue.size());
((Map<?, ?>) data).forEach((k, v) -> val.put(getEventColumn(event, String.valueOf(k)), v));
return val;
}
return Collections.singletonMap(getEventColumn(event, "value"), data);
}
return null;
});
if (CollectionUtils.isEmpty(properties)) {
return;
}
String productId = message.getHeader("productId").map(String::valueOf).orElse("null");
String deviceName = message.getHeader("deviceName").map(String::valueOf).orElse(message.getDeviceId());
String tableName = getLatestTableTableName(productId);
Map<String, Object> prob = new HashMap<>(properties);
prob.put("id", message.getDeviceId());
prob.put("deviceName", deviceName);
Buffer buffer = Buffer.of(tableName, message.getDeviceId(), deviceName, prob, message.getTimestamp());
writer.write(buffer);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
@Getter
private static class Buffer implements Externalizable {
//有效期
private final static long expires = Duration.ofSeconds(30).toMillis();
private String table;
private String deviceId;
private String deviceName;
private Map<String, Object> properties;
private long timestamp;
public Buffer() {
}
public boolean isEffective() {
return System.currentTimeMillis() - timestamp < expires;
}
public static Buffer of(String table,
String deviceId,
String deviceName,
Map<String, Object> properties,
long timestamp) {
Buffer buffer = new Buffer();
buffer.table = table;
buffer.deviceId = deviceId;
buffer.deviceName = deviceName;
buffer.properties = properties;
buffer.timestamp = timestamp;
return buffer;
}
public Buffer merge(Buffer buffer) {
//以比较新的数据为准
if (buffer.timestamp > this.timestamp) {
return buffer.merge(this);
}
//合并
buffer.properties.forEach(properties::putIfAbsent);
return this;
}
int size() {
return properties == null ? 0 : properties.size();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(table);
out.writeUTF(deviceId);
out.writeUTF(deviceName);
out.writeLong(timestamp);
SerializeUtils.writeObject(properties, out);
}
@Override
@SuppressWarnings("all")
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
table = in.readUTF();
deviceId = in.readUTF();
deviceName = in.readUTF();
timestamp = in.readLong();
properties = (Map<String, Object>) SerializeUtils.readObject(in);
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Mono<Void> doUpdateLatestData(String table,
List<Map<String, Object>> properties) {
return databaseOperator
.getMetadata()
.getCurrentSchema()
.getTableReactive(table, false)
.flatMap(ignore -> {
//没有deviceName,说明可能在同步表结构的时候发生了错误
if (!ignore.getColumn("deviceName").isPresent()) {
log.warn("设备最新数据表[{}]结构错误", table);
return Mono.empty();
}
return databaseOperator
.dml()
.upsert(table)
.ignoreUpdate("id")
.values(properties)
.execute()
.reactive()
.then();
});
}
public ReactiveRepository<Record, String> getRepository(String productId) {
return databaseOperator
.dml()
.createReactiveRepository(getLatestTableTableName(productId));
}
@Override
public Flux<DeviceLatestData> query(String productId, QueryParamEntity param) {
return getRepository(productId)
.createQuery()
.setParam(param)
.fetch()
.map(DeviceLatestData::new);
}
@Override
public Mono<DeviceLatestData> queryDeviceData(String productId, String deviceId) {
return getRepository(productId)
.findById(deviceId)
.map(DeviceLatestData::new);
}
@Override
public Mono<Integer> count(String productId, QueryParamEntity param) {
return getRepository(productId)
.createQuery()
.setParam(param)
.count();
}
private SelectColumnSupplier createAggColumn(AggregationColumn column) {
switch (column.getAggregation()) {
case COUNT:
return Selects.count(column.getProperty()).as(column.getAlias());
case AVG:
return Selects.avg(column.getProperty()).as(column.getAlias());
case MAX:
return Selects.max(column.getProperty()).as(column.getAlias());
case MIN:
return Selects.min(column.getProperty()).as(column.getAlias());
case SUM:
return Selects.sum(column.getProperty()).as(column.getAlias());
default:
throw new UnsupportedOperationException("unsupported agg:" + column.getAggregation());
}
}
private SelectColumnSupplier[] createAggColumns(List<AggregationColumn> columns) {
return columns
.stream()
.map(this::createAggColumn)
.toArray(SelectColumnSupplier[]::new);
}
@Override
public Mono<Map<String, Object>> aggregation(String productId,
List<AggregationColumn> columns,
QueryParamEntity paramEntity) {
if (CollectionUtils.isEmpty(columns)) {
return Mono.error(new ValidationException("columns", "error.aggregate_column_cannot_be_empty"));
}
String table = getLatestTableTableName(productId);
return databaseOperator
.getMetadata()
.getTableReactive(table)
.flatMap(tableMetadata ->
{
List<String> illegals = new ArrayList<>();
List<AggregationColumn> columnList = columns
.stream()
.filter(column -> {
if (tableMetadata
.getColumn(column.getProperty())
.isPresent()) {
return true;
}
illegals.add(column.getProperty());
return false;
})
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(columnList)) {
return Mono.error(new ValidationException("columns", "error.invalid_product_attribute_or_event", productId, illegals));
}
return databaseOperator
.dml()
.query(table)
.select(createAggColumns(columnList))
.setParam(paramEntity.clone().noPaging())
.fetch(ResultWrappers.map())
.reactive()
.take(1)
.singleOrEmpty()
.doOnNext(map -> {
for (AggregationColumn column : columns) {
map.putIfAbsent(column.getAlias(), 0);
}
})
//表不存在
.onErrorReturn(e -> StringUtils.hasText(e.getMessage()) && e
.getMessage()
.contains("doesn't exist "), Collections.emptyMap());
}
);
}
@Override
public Flux<Map<String, Object>> aggregation(Flux<QueryProductLatestDataRequest> param,
boolean merge) {
Flux<QueryProductLatestDataRequest> cached = param.cache();
return cached
.flatMap(request -> this
.aggregation(request.getProductId(), request.getColumns(), request.getQuery())
.doOnNext(map -> {
if (!merge) {
map.put("productId", request.getProductId());
}
}))
.as(flux -> {
if (!merge) {
return flux;
}
//合并所有产品的字段到一条数据中,合并时,使用第一个聚合字段使用的聚合类型
return cached
.take(1)
.flatMapIterable(QueryLatestDataRequest::getColumns)
.collectMap(AggregationColumn::getAlias, agg -> aggMappers.getOrDefault(agg.getAggregation(), sum))
.flatMap(mappers -> flux
.flatMapIterable(Map::entrySet)
.groupBy(Map.Entry::getKey, Integer.MAX_VALUE)
.flatMap(group -> mappers
.getOrDefault(group.key(), sum)
.apply(group.map(Map.Entry::getValue))
.map(val -> Tuples.of(String.valueOf(group.key()), (Object) val)))
.collectMap(Tuple2::getT1, Tuple2::getT2)).flux();
});
}
static Map<Aggregation, Function<Flux<Object>, Mono<? extends Number>>> aggMappers = new HashMap<>();
static Function<Flux<Object>, Mono<? extends Number>> avg = flux -> MathFlux.averageDouble(flux
.map(CastUtils::castNumber)
.map(Number::doubleValue));
static Function<Flux<Object>, Mono<? extends Number>> max = flux -> MathFlux.max(flux
.map(CastUtils::castNumber)
.map(Number::doubleValue));
static Function<Flux<Object>, Mono<? extends Number>> min = flux -> MathFlux.min(flux
.map(CastUtils::castNumber)
.map(Number::doubleValue));
static Function<Flux<Object>, Mono<? extends Number>> sum = flux -> MathFlux.sumDouble(flux
.map(CastUtils::castNumber)
.map(Number::doubleValue));
static {
aggMappers.put(Aggregation.AVG, avg);
aggMappers.put(Aggregation.MAX, max);
aggMappers.put(Aggregation.MIN, min);
aggMappers.put(Aggregation.SUM, sum);
aggMappers.put(Aggregation.COUNT, sum);
}
}

View File

@ -11,6 +11,7 @@ import org.jetlinks.community.device.entity.DeviceOperationLogEntity;
import org.jetlinks.community.device.entity.DeviceProperty;
import org.jetlinks.community.timeseries.query.Aggregation;
import org.jetlinks.community.timeseries.query.AggregationData;
import org.jetlinks.core.config.ConfigKey;
import org.jetlinks.core.message.DeviceMessage;
import org.jetlinks.core.metadata.DeviceMetadata;
import org.jetlinks.core.metadata.EventMetadata;
@ -33,6 +34,9 @@ import java.util.Map;
*/
public interface DeviceDataService {
ConfigKey<String> STORE_POLICY_CONFIG_KEY = ConfigKey.of("storePolicy", "存储策略", String.class);
/**
* 注册设备物模型信息
*

View File

@ -0,0 +1,138 @@
package org.jetlinks.community.device.service.data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.web.api.crud.entity.PagerResult;
import org.hswebframework.web.api.crud.entity.QueryParamEntity;
import org.jetlinks.core.message.DeviceMessage;
import org.jetlinks.core.metadata.DeviceMetadata;
import org.jetlinks.community.device.entity.DeviceLatestData;
import org.jetlinks.community.doc.QueryConditionOnly;
import org.jetlinks.community.timeseries.query.AggregationColumn;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;
/**
* 设备最新数据服务,用于保存设备最新的相关数据到关系型数据库中可以使用动态条件进行查询相关数据
*
* @author zhouhao
* @since 1.5.0
*/
public interface DeviceLatestDataService {
/**
* 根据物模型更新产品表结构信息
*
* @param productId 产品ID
* @param metadata 物模型
* @return void
*/
Mono<Void> upgradeMetadata(String productId, DeviceMetadata metadata);
/**
* 重新加载物模型信息
*
* @param productId 产品ID
* @param metadata 物模型
* @return void
*/
Mono<Void> reloadMetadata(String productId, DeviceMetadata metadata);
/**
* 保存消息数据
*
* @param message 设备消息
*/
void save(DeviceMessage message);
/**
* 根据产品ID 查询最新的数据
*
* @param productId 产品ID
* @param param 查询参数
* @return 数据列表
*/
Flux<DeviceLatestData> query(String productId, QueryParamEntity param);
/**
* 查询设备最新属性数据
*
* @param productId 产品ID
* @param deviceId 设备ID
* @return 属性数据
*/
Mono<DeviceLatestData> queryDeviceData(String productId, String deviceId);
/**
* 根据产品ID查询数量
*
* @param productId 产品ID
* @param param 参数
* @return 查询数量
*/
Mono<Integer> count(String productId, QueryParamEntity param);
/**
* 根据产品ID分页查询数据
*
* @param productId 产品ID
* @param param 查询条件参数
* @return 分页结果数据
*/
default Mono<PagerResult<DeviceLatestData>> queryPager(String productId, QueryParamEntity param) {
return Mono
.zip(
query(productId, param).collectList(),
count(productId, param),
(data, total) -> PagerResult.of(total, data, param)
)
.defaultIfEmpty(PagerResult.empty());
}
/**
* 根据产品ID聚合查询数据
*
* @param productId 产品ID
* @param columns 聚合列
* @param paramEntity 查询条件参数
* @return 聚合结果
*/
Mono<Map<String, Object>> aggregation(String productId,
List<AggregationColumn> columns,
QueryParamEntity paramEntity);
/**
* 聚合查询多个产品下设备最新的数据
*
* @param param 参数
* @param merge 是否将所有数据合并在一起
* @return 查询结果
*/
Flux<Map<String, Object>> aggregation(Flux<QueryProductLatestDataRequest> param,
boolean merge);
@Getter
@Setter
class QueryProductLatestDataRequest extends QueryLatestDataRequest {
@NotBlank
@Schema(defaultValue = "产品ID")
private String productId;
}
@Getter
@Setter
class QueryLatestDataRequest {
@NotNull
private List<AggregationColumn> columns;
@Schema(implementation = QueryConditionOnly.class)
private QueryParamEntity query = QueryParamEntity.of();
}
}

View File

@ -0,0 +1,54 @@
package org.jetlinks.community.device.service.data;
import org.hswebframework.web.api.crud.entity.QueryParamEntity;
import org.jetlinks.core.message.DeviceMessage;
import org.jetlinks.core.metadata.DeviceMetadata;
import org.jetlinks.community.device.entity.DeviceLatestData;
import org.jetlinks.community.timeseries.query.AggregationColumn;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
public class NonDeviceLatestDataService implements DeviceLatestDataService {
@Override
public Mono<Void> upgradeMetadata(String productId, DeviceMetadata metadata) {
return Mono.empty();
}
@Override
public Mono<Void> reloadMetadata(String productId, DeviceMetadata metadata) {
return Mono.empty();
}
@Override
public void save(DeviceMessage message) {
}
@Override
public Flux<DeviceLatestData> query(String productId, QueryParamEntity param) {
return Flux.empty();
}
@Override
public Mono<DeviceLatestData> queryDeviceData(String productId, String deviceId) {
return Mono.empty();
}
@Override
public Mono<Integer> count(String productId, QueryParamEntity param) {
return Mono.empty();
}
@Override
public Mono<Map<String, Object>> aggregation(String productId, List<AggregationColumn> columns, QueryParamEntity paramEntity) {
return Mono.empty();
}
@Override
public Flux<Map<String, Object>> aggregation(Flux<QueryProductLatestDataRequest> param, boolean merge) {
return Flux.empty();
}
}

View File

@ -0,0 +1,38 @@
package org.jetlinks.community.device.service.data;
import org.jetlinks.core.metadata.PropertyMetadata;
public interface StorageConstants {
String storePolicyConfigKey = "storePolicy";
String propertyStorageType = "storageType";
String propertyStorageTypeJson = "json-string";
String propertyStorageTypeIgnore = "ignore";
/**
* 判断属性是否使用json字符串来存储
*
* @param metadata 属性物模型
* @return 是否使用json字符串存储
*/
static boolean propertyIsJsonStringStorage(PropertyMetadata metadata) {
return metadata
.getExpand(propertyStorageType)
.map(propertyStorageTypeJson::equals)
.orElse(false);
}
/**
* 判断属性是否忽略存储
*
* @param metadata 属性物模型
* @return 属性是否忽略存储
*/
static boolean propertyIsIgnoreStorage(PropertyMetadata metadata) {
return metadata
.getExpand(propertyStorageType)
.map(propertyStorageTypeIgnore::equals)
.orElse(false);
}
}

View File

@ -0,0 +1,94 @@
package org.jetlinks.community.device.service.data;
import lombok.AllArgsConstructor;
import org.jetlinks.core.Value;
import org.jetlinks.core.device.DeviceRegistry;
import org.jetlinks.core.metadata.*;
import org.jetlinks.core.metadata.types.ArrayType;
import org.jetlinks.core.metadata.types.EnumType;
import org.jetlinks.core.metadata.types.ObjectType;
import org.jetlinks.community.device.spi.DeviceConfigMetadataSupplier;
import org.jetlinks.community.things.data.ThingsDataRepositoryStrategies;
import org.jetlinks.community.things.data.ThingsDataRepositoryStrategy;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Component
@AllArgsConstructor
public class StorageDeviceConfigMetadataSupplier implements DeviceConfigMetadataSupplier {
private final DeviceRegistry registry;
private final DeviceDataStorageProperties properties;
private final ConfigMetadata objectConf = new DefaultConfigMetadata("存储配置", "")
.scope(DeviceConfigScope.product)
.add(StorageConstants.propertyStorageType, "存储方式", new EnumType()
.addElement(EnumType.Element.of("direct", "直接存储", "直接存储上报的数据"))
.addElement(EnumType.Element.of(StorageConstants.propertyStorageTypeIgnore, "不存储", "不存储此属性值"))
.addElement(EnumType.Element.of(StorageConstants.propertyStorageTypeJson, "JSON字符", "将数据序列话为JSON字符串进行存储"))
);
private final ConfigMetadata anotherConf = new DefaultConfigMetadata("存储配置", "")
.scope(DeviceConfigScope.product)
.add(StorageConstants.propertyStorageType, "存储方式", new EnumType()
.addElement(EnumType.Element.of("direct", "存储", "将上报的属性值保存到配置到存储策略中"))
.addElement(EnumType.Element.of(StorageConstants.propertyStorageTypeIgnore, "不存储", "不存储此属性值"))
);
@Override
public Flux<ConfigMetadata> getDeviceConfigMetadata(String deviceId) {
return Flux.empty();
}
@Override
public Flux<ConfigMetadata> getDeviceConfigMetadataByProductId(String productId) {
return Flux.empty();
}
@Override
public Flux<ConfigMetadata> getProductConfigMetadata(String productId) {
return Flux.empty();
}
@Override
public Flux<Feature> getProductFeatures(String productId) {
return registry
.getProduct(productId)
.flatMap(prod -> prod.getConfig(DeviceDataService.STORE_POLICY_CONFIG_KEY))
.defaultIfEmpty(properties.getDefaultPolicy())
.flatMap(this::getStoragePolicy)
.flatMapMany(strategy -> strategy
.opsForSave(ThingsDataRepositoryStrategy.OperationsContext.DEFAULT)
.getFeatures());
}
private Mono<ThingsDataRepositoryStrategy> getStoragePolicy(String policy) {
return Mono.justOrEmpty(ThingsDataRepositoryStrategies.getStrategy(policy));
}
@Override
public Flux<ConfigMetadata> getMetadataExpandsConfig(String productId,
DeviceMetadataType metadataType,
String metadataId,
String typeId) {
if (metadataType == DeviceMetadataType.property) {
if ((ObjectType.ID.equals(typeId) || ArrayType.ID.equals(typeId))) {
return registry
.getProduct(productId)
.flatMap(prod -> prod
.getConfig(StorageConstants.storePolicyConfigKey)
.map(Value::asString))
.defaultIfEmpty(properties.getDefaultPolicy())
.filter(policy -> policy.startsWith("default-"))
.map(ignore -> objectConf)
.flux();
}
return Flux.just(anotherConf);
}
return Flux.empty();
}
}

View File

@ -1,7 +1,9 @@
package org.jetlinks.community.device.spi;
import lombok.Generated;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.jetlinks.core.metadata.DeviceMetadataType;
import org.jetlinks.core.metadata.Feature;
import reactor.core.publisher.Flux;
/**
@ -38,4 +40,13 @@ public interface DeviceConfigMetadataSupplier {
String typeId) {
return Flux.empty();
}
/**
* @see org.jetlinks.community.device.service.DeviceConfigMetadataManager#getProductFeatures(String)
*/
@Generated
default Flux<Feature> getProductFeatures(String productId){
return Flux.empty();
}
}

View File

@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.hswebframework.ezorm.rdb.exception.DuplicateKeyException;
import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult;
@ -22,6 +23,7 @@ import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
import org.hswebframework.web.exception.BusinessException;
import org.hswebframework.web.exception.NotFoundException;
import org.hswebframework.web.exception.ValidationException;
import org.hswebframework.web.i18n.LocaleUtils;
import org.hswebframework.web.id.IDGenerator;
import org.jetlinks.community.device.entity.*;
import org.jetlinks.community.device.enums.DeviceState;
@ -34,10 +36,16 @@ import org.jetlinks.community.device.service.LocalDeviceProductService;
import org.jetlinks.community.device.service.data.DeviceDataService;
import org.jetlinks.community.device.web.excel.DeviceExcelInfo;
import org.jetlinks.community.device.web.excel.DeviceWrapper;
import org.jetlinks.community.device.web.excel.PropertyMetadataExcelInfo;
import org.jetlinks.community.device.web.excel.PropertyMetadataWrapper;
import org.jetlinks.community.device.web.request.AggRequest;
import org.jetlinks.community.io.excel.ImportExportService;
import org.jetlinks.community.io.utils.FileUtils;
import org.jetlinks.community.relation.RelationObjectProvider;
import org.jetlinks.community.relation.service.RelationService;
import org.jetlinks.community.relation.service.request.SaveRelationRequest;
import org.jetlinks.community.timeseries.query.AggregationData;
import org.jetlinks.community.web.response.ValidationResult;
import org.jetlinks.core.Values;
import org.jetlinks.core.device.*;
import org.jetlinks.core.device.manager.DeviceBindHolder;
@ -48,6 +56,7 @@ import org.jetlinks.core.message.Message;
import org.jetlinks.core.message.MessageType;
import org.jetlinks.core.message.RepayableDeviceMessage;
import org.jetlinks.core.metadata.*;
import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.data.util.Lazy;
@ -73,7 +82,7 @@ import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.hswebframework.reactor.excel.ReactorExcel.*;
import static org.hswebframework.reactor.excel.ReactorExcel.read;
@RestController
@RequestMapping({"/device-instance", "/device/instance"})
@ -99,6 +108,8 @@ public class DeviceInstanceController implements
private final DeviceConfigMetadataManager metadataManager;
private final RelationService relationService;
@SuppressWarnings("all")
public DeviceInstanceController(LocalDeviceInstanceService service,
DeviceRegistry registry,
@ -106,7 +117,8 @@ public class DeviceInstanceController implements
ImportExportService importExportService,
ReactiveRepository<DeviceTagEntity, String> tagRepository,
DeviceDataService deviceDataService,
DeviceConfigMetadataManager metadataManager) {
DeviceConfigMetadataManager metadataManager,
RelationService relationService) {
this.service = service;
this.registry = registry;
this.productService = productService;
@ -114,6 +126,7 @@ public class DeviceInstanceController implements
this.tagRepository = tagRepository;
this.deviceDataService = deviceDataService;
this.metadataManager = metadataManager;
this.relationService = relationService;
}
@ -125,6 +138,17 @@ public class DeviceInstanceController implements
return service.getDeviceDetail(id);
}
//读取设备属性
@PostMapping("/{deviceId:.+}/properties/_read")
@QueryAction
@Operation(summary = "发送读取属性指令到设备", description = "请求示例: [\"属性ID\"]")
public Mono<?> readProperties(@PathVariable @Parameter(description = "设备ID") String deviceId,
@RequestBody Mono<List<String>> properties) {
return properties.flatMap(props -> service.readProperties(deviceId, props));
}
//获取设备详情
@GetMapping("/{id:.+}/config-metadata")
@QueryAction
@ -314,12 +338,22 @@ public class DeviceInstanceController implements
//查询设备日志
@GetMapping("/{deviceId:.+}/logs")
@QueryAction
@QueryOperation(summary = "查询设备日志数据")
@QueryOperation(summary = "(GET)查询设备日志数据")
public Mono<PagerResult<DeviceOperationLogEntity>> queryDeviceLog(@PathVariable @Parameter(description = "设备ID") String deviceId,
@Parameter(hidden = true) QueryParamEntity entity) {
return deviceDataService.queryDeviceMessageLog(deviceId, entity);
}
//查询设备日志
@PostMapping("/{deviceId:.+}/logs")
@QueryAction
@Operation(summary = "(POST)查询设备日志数据")
public Mono<PagerResult<DeviceOperationLogEntity>> queryDeviceLog(@PathVariable @Parameter(description = "设备ID") String deviceId,
@RequestBody @Parameter(hidden = true) Mono<QueryParamEntity> queryParam) {
return queryParam.flatMap(param -> deviceDataService.queryDeviceMessageLog(deviceId, param));
}
//删除标签
@DeleteMapping("/{deviceId}/tag/{tagId:.+}")
@SaveAction
@ -366,8 +400,7 @@ public class DeviceInstanceController implements
* 批量激活设备
*
* @param idList ID列表
* @return 被注销的数量
* @since 1.1
* @return 被激活的数量
*/
@PutMapping("/batch/_deploy")
@SaveAction
@ -844,4 +877,92 @@ public class DeviceInstanceController implements
}
@GetMapping("/{id:.+}/exists")
@QueryAction
@Operation(summary = "验证设备ID是否存在")
public Mono<Boolean> deviceIdValidate(@PathVariable @Parameter(description = "设备ID") String id) {
return service.findById(id)
.hasElement();
}
@GetMapping("/id/_validate")
@QueryAction
@Operation(summary = "验证设备ID是否合法")
public Mono<ValidationResult> deviceIdValidate2(@RequestParam @Parameter(description = "设备ID") String id) {
return LocaleUtils.currentReactive()
.flatMap(locale -> {
DeviceInstanceEntity entity = new DeviceInstanceEntity();
entity.setId(id);
entity.validateId();
return service.findById(id)
.map(device -> ValidationResult.error(
LocaleUtils.resolveMessage("error.device_ID_already_exists", locale)))
.defaultIfEmpty(ValidationResult.success());
})
.onErrorResume(ValidationException.class, e -> Mono.just(e.getI18nCode())
.map(ValidationResult::error));
}
//解析文件为属性物模型
@PostMapping(value = "/{productId}/property-metadata/import")
@SaveAction
@Operation(summary = "解析文件为属性物模型")
public Mono<String> importPropertyMetadata(@PathVariable @Parameter(description = "产品ID") String productId,
@RequestParam @Parameter(description = "文件地址,支持csv,xlsx文件格式") String fileUrl) {
return metadataManager
.getMetadataExpandsConfig(productId, DeviceMetadataType.property, "*", "*", DeviceConfigScope.device)
.collectList()
.map(PropertyMetadataWrapper::new)
//解析数据并转为物模型
.flatMap(wrapper -> importExportService
.getInputStream(fileUrl)
.flatMapMany(inputStream -> read(inputStream, FileUtils.getExtension(fileUrl), wrapper))
.map(PropertyMetadataExcelInfo::toMetadata)
.collectList())
.filter(CollectionUtils::isNotEmpty)
.map(list -> {
SimpleDeviceMetadata metadata = new SimpleDeviceMetadata();
list.forEach(metadata::addProperty);
return JetLinksDeviceMetadataCodec.getInstance().doEncode(metadata);
});
}
//获取物模型属性导入模块
@GetMapping("/{deviceId}/property-metadata/template.{format}")
@QueryAction
@Operation(summary = "下载设备物模型属性导入模块")
public Mono<Void> downloadMetadataExportTemplate(@PathVariable @Parameter(description = "设备ID") String deviceId,
ServerHttpResponse response,
@PathVariable @Parameter(description = "文件格式,支持csv,xlsx") String format) throws IOException {
response.getHeaders().set(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=".concat(URLEncoder.encode("物模型导入模块." + format, StandardCharsets.UTF_8
.displayName())));
return metadataManager
.getMetadataExpandsConfig(deviceId, DeviceMetadataType.property, "*", "*", DeviceConfigScope.device)
.collectList()
.map(PropertyMetadataExcelInfo::getTemplateHeaderMapping)
.flatMapMany(headers ->
ReactorExcel.<DeviceExcelInfo>writer(format)
.headers(headers)
.converter(DeviceExcelInfo::toMap)
.writeBuffer(Flux.empty()))
.doOnError(err -> log.error(err.getMessage(), err))
.map(bufferFactory::wrap)
.as(response::writeWith)
;
}
@PatchMapping("/{deviceId}/relations")
@Operation(summary = "保存设备的关系信息")
@SaveAction
public Mono<Void> saveRelation(@PathVariable String deviceId,
@RequestBody Flux<SaveRelationRequest> requestFlux) {
return relationService.saveRelated(RelationObjectProvider.TYPE_DEVICE, deviceId, requestFlux);
}
}

View File

@ -7,34 +7,51 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.hswebframework.reactor.excel.ReactorExcel;
import org.hswebframework.web.authorization.annotation.QueryAction;
import org.hswebframework.web.authorization.annotation.Resource;
import org.hswebframework.web.authorization.annotation.SaveAction;
import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
import org.hswebframework.web.exception.ValidationException;
import org.hswebframework.web.i18n.LocaleUtils;
import org.jetlinks.community.device.entity.DeviceProductEntity;
import org.jetlinks.community.device.service.DeviceConfigMetadataManager;
import org.jetlinks.community.device.service.LocalDeviceProductService;
import org.jetlinks.community.device.service.data.DeviceDataService;
import org.jetlinks.community.device.web.excel.PropertyMetadataExcelInfo;
import org.jetlinks.community.device.web.excel.PropertyMetadataWrapper;
import org.jetlinks.community.device.web.request.AggRequest;
import org.jetlinks.community.io.excel.ImportExportService;
import org.jetlinks.community.io.utils.FileUtils;
import org.jetlinks.community.things.data.ThingsDataRepositoryStrategy;
import org.jetlinks.community.timeseries.query.AggregationData;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.jetlinks.core.metadata.DeviceConfigScope;
import org.jetlinks.core.metadata.DeviceMetadataCodec;
import org.jetlinks.core.metadata.DeviceMetadataType;
import org.jetlinks.community.web.response.ValidationResult;
import org.jetlinks.core.metadata.*;
import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import static org.hswebframework.reactor.excel.ReactorExcel.read;
@RestController
@RequestMapping({"/device-product","/device/product"})
@Resource(id = "device-product", name = "设备产品")
@Tag(name = "设备产品接口")
@Slf4j
public class DeviceProductController implements ReactiveServiceCrudController<DeviceProductEntity, String> {
private final LocalDeviceProductService productService;
@ -48,16 +65,23 @@ public class DeviceProductController implements ReactiveServiceCrudController<De
private final DeviceMetadataCodec defaultCodec = new JetLinksDeviceMetadataCodec();
private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
private final ImportExportService importExportService;
public DeviceProductController(LocalDeviceProductService productService,
List<ThingsDataRepositoryStrategy> policies,
DeviceDataService deviceDataService,
DeviceConfigMetadataManager configMetadataManager,
ObjectProvider<DeviceMetadataCodec> metadataCodecs) {
ObjectProvider<DeviceMetadataCodec> metadataCodecs,
ImportExportService importExportService) {
this.productService = productService;
this.policies = policies;
this.deviceDataService = deviceDataService;
this.configMetadataManager = configMetadataManager;
this.metadataCodecs = metadataCodecs;
this.importExportService = importExportService;
}
@Override
@ -177,4 +201,82 @@ public class DeviceProductController implements ReactiveServiceCrudController<De
}
}
@GetMapping("/{id:.+}/exists")
@QueryAction
@Operation(summary = "验证产品ID是否存在")
public Mono<Boolean> deviceIdValidate(@PathVariable @Parameter(description = "产品ID") String id) {
return productService.findById(id)
.hasElement();
}
@GetMapping("/id/_validate")
@QueryAction
@Operation(summary = "验证产品ID是否合法")
public Mono<ValidationResult> deviceIdValidate2(@RequestParam @Parameter(description = "产品ID") String id) {
return LocaleUtils.currentReactive()
.flatMap(locale -> {
DeviceProductEntity entity = new DeviceProductEntity();
entity.setId(id);
entity.validateId();
return productService.findById(id)
.map(product -> ValidationResult.error(
LocaleUtils.resolveMessage("error.product_ID_already_exists", locale)))
.defaultIfEmpty(ValidationResult.success());
})
.onErrorResume(ValidationException.class, e -> Mono.just(e.getI18nCode())
.map(ValidationResult::error));
}
//获取产品物模型属性导入模块
@GetMapping("/{productId}/property-metadata/template.{format}")
@QueryAction
@Operation(summary = "下载产品物模型属性导入模块")
public Mono<Void> downloadExportPropertyMetadataTemplate(@PathVariable @Parameter(description = "产品ID") String productId,
ServerHttpResponse response,
@PathVariable @Parameter(description = "文件格式,支持csv,xlsx") String format) throws IOException {
response.getHeaders().set(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=".concat(URLEncoder.encode("物模型导入模块." + format, StandardCharsets.UTF_8
.displayName())));
return configMetadataManager
.getMetadataExpandsConfig(productId, DeviceMetadataType.property, "*", "*", DeviceConfigScope.product)
.collectList()
.map(PropertyMetadataExcelInfo::getTemplateHeaderMapping)
.flatMapMany(headers -> ReactorExcel
.<PropertyMetadataExcelInfo>writer(format)
.headers(headers)
.converter(PropertyMetadataExcelInfo::toMap)
.writeBuffer(PropertyMetadataExcelInfo.getTemplateContentMapping()))
.doOnError(err -> log.error(err.getMessage(), err))
.map(bufferFactory::wrap)
.as(response::writeWith)
;
}
//解析文件为属性物模型
@PostMapping(value = "/{productId}/property-metadata/import")
@SaveAction
@Operation(summary = "解析文件为属性物模型")
public Mono<String> importPropertyMetadata(@PathVariable @Parameter(description = "产品ID") String productId,
@RequestParam @Parameter(description = "文件地址,支持csv,xlsx文件格式") String fileUrl) {
return configMetadataManager
.getMetadataExpandsConfig(productId, DeviceMetadataType.property, "*", "*", DeviceConfigScope.product)
.collectList()
.map(PropertyMetadataWrapper::new)
//解析数据并转为物模型
.flatMap(wrapper -> importExportService
.getInputStream(fileUrl)
.flatMapMany(inputStream -> read(inputStream, FileUtils.getExtension(fileUrl), wrapper))
.map(PropertyMetadataExcelInfo::toMetadata)
.collectList())
.filter(CollectionUtils::isNotEmpty)
.map(list -> {
SimpleDeviceMetadata metadata = new SimpleDeviceMetadata();
list.forEach(metadata::addProperty);
return JetLinksDeviceMetadataCodec.getInstance().doEncode(metadata);
});
}
}

View File

@ -0,0 +1,321 @@
package org.jetlinks.community.device.web.excel;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.hswebframework.reactor.excel.CellDataType;
import org.hswebframework.reactor.excel.ExcelHeader;
import org.hswebframework.web.bean.FastBeanCopier;
import org.hswebframework.web.dict.EnumDict;
import org.hswebframework.web.exception.BusinessException;
import org.hswebframework.web.exception.ValidationException;
import org.hswebframework.web.validator.ValidatorUtils;
import org.jetlinks.core.metadata.*;
import org.jetlinks.core.metadata.types.*;
import org.jetlinks.core.metadata.unit.ValueUnit;
import org.jetlinks.core.metadata.unit.ValueUnits;
import org.jetlinks.supports.official.JetLinksDataTypeCodecs;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import javax.validation.constraints.NotBlank;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@Getter
@Setter
@Slf4j
public class PropertyMetadataExcelInfo {
@NotBlank(message = "属性ID不能为空")
private String property;
@NotBlank(message = "属性名称不能为空")
private String name;
private String valueType;
private Map<String, Object> expands;
//数据类型
@NotBlank(message = "数据类型不能为空")
private String dataType;
//单位
private String unit;
//精度
private String scale;
//来源
@NotBlank(message = "来源不能为空")
private String source;
private String description;
private String storageType;
private long rowNumber;
//读写类型
private List<String> type;
/**
* 单位
*/
private static final List<ValueUnit> idList = ValueUnits.getAllUnit();
/**
* 所有数据类型
*/
private static final List<String> DATA_TYPES = Lists.newArrayList(ArrayType.ID, BooleanType.ID,
DateTimeType.ID, DoubleType.ID, EnumType.ID, FloatType.ID, IntType.ID, LongType.ID,
ObjectType.ID, StringType.ID, GeoType.ID, FileType.ID, PasswordType.ID, GeoShapeType.ID);
private static final List<String> OBJECT_NOT_HAVE = Lists.newArrayList(DateTimeType.ID, FileType.ID, ObjectType.ID, PasswordType.ID);
/**
* 简单模板支持类型
*/
private static final List<String> SIMPLE = Lists.newArrayList(IntType.ID, FloatType.ID, DoubleType.ID, LongType.ID);
public void with(String key, Object value) {
FastBeanCopier.copy(Collections.singletonMap(key, value), this);
}
public PropertyMetadata toMetadata() {
SimplePropertyMetadata metadata = new SimplePropertyMetadata();
try {
ValidatorUtils.tryValidate(this);
if (CollectionUtils.isEmpty(type) || type.size() == 1 && StringUtils.isEmpty(type.get(0))) {
throw new ValidationException("读写类型不能为空");
}
metadata.setId(property);
metadata.setName(name);
metadata.setValueType(parseDataType());
metadata.setExpands(parseExpands());
metadata.setDescription(description);
return metadata;
} catch (Throwable e) {
throw new BusinessException("" + this.getRowNumber() + "行错误:" + e.getMessage());
}
}
public static List<ExcelHeader> getTemplateHeaderMapping(List<ConfigMetadata> configMetadataList) {
List<ExcelHeader> arr = new ArrayList<>(Arrays.asList(
new ExcelHeader("property", "属性ID", CellDataType.STRING),
new ExcelHeader("name", "属性名称", CellDataType.STRING),
new ExcelHeader("dataType", "数据类型", CellDataType.STRING),
new ExcelHeader("unit", "单位", CellDataType.STRING),
new ExcelHeader("scale", "精度", CellDataType.STRING),
new ExcelHeader("valueType", "数据类型配置", CellDataType.STRING),
new ExcelHeader("source", "来源", CellDataType.STRING),
new ExcelHeader("description", "属性说明", CellDataType.STRING),
new ExcelHeader("type", "读写类型", CellDataType.STRING)
));
Set<String> expandsKeys = new HashSet<>();
for (ConfigMetadata configMetadata : configMetadataList) {
for (ConfigPropertyMetadata property : configMetadata.getProperties()) {
String header = property.getName();
if (expandsKeys.contains(header)) {
header = configMetadata.getName() + "-" + header;
}
arr.add(new ExcelHeader("expands." + property.getProperty(), header, CellDataType.STRING));
expandsKeys.add(property.getName());
}
}
return arr;
}
protected DataType parseDataType() {
JSONObject dataTypeJson = new JSONObject();
//默认先使用json格式的数据解析物模型没有json则使用简单模板只支持int long double float
if (!StringUtils.isEmpty(this.valueType)) {
dataTypeJson = JSON.parseObject(this.valueType);
this.dataType = dataTypeJson.getString("type");
} else {
dataTypeJson.put("type", this.dataType);
dataTypeJson.put("unit", this.unit);
dataTypeJson.put("scale", this.scale);
}
DataType dataType = Optional.ofNullable(this.dataType)
.map(DataTypes::lookup)
.map(Supplier::get)
.orElseThrow(() -> new BusinessException("error.unknown_data_type" ,500, this, getDataType()));
JSONObject finalDataTypeJson = dataTypeJson;
JetLinksDataTypeCodecs
.getCodec(dataType.getId())
.ifPresent(codec -> codec.decode(dataType, finalDataTypeJson));
return dataType;
}
protected Map<String, Object> parseExpands() {
Map<String, Object> map = new HashMap<>(4);
map.put("source", PropertySource.getValue(source));
map.put("storageType", PropertyStorage.getValue(storageType));
map.put("tags", "");
map.put("type", type.stream().map(PropertyType::getValue).collect(Collectors.toList()));
return map;
}
public static Flux<PropertyMetadataExcelInfo> getTemplateContentMapping() {
return Flux.fromIterable(DATA_TYPES)
.flatMap(dt -> {
PropertyMetadataExcelInfo excelInfo = new PropertyMetadataExcelInfo();
DataType dataType = DataTypes.lookup(dt).get();
excelInfo.setProperty(dataType.getType() + "_id");
excelInfo.setName(dataType.getType() + "类型属性示例");
excelInfo.setDataType(dataType.getId());
excelInfo.setUnit("");
excelInfo.setScale("");
Random random = new Random();
excelInfo.setStorageType(random.nextBoolean() ? "direct" : "ignore");
excelInfo.setSource(random.nextInt(2) == 1 ? "manual" : random.nextInt(2) < 1 ? "device" : "rule");
excelInfo.setDescription(excelInfo.getName() + "的说明");
if (SIMPLE.contains(dt)) {
excelInfo.setUnit(idList.get(0).getId());
excelInfo.setDataType(dt);
excelInfo.setScale(String.valueOf(random.nextInt(2)));
excelInfo.setDescription(excelInfo.getName() + "的说明,优先使用json数据类型配置没有则使用简单模板仅支持int double float long四种");
}
Map<String, Object> valueType = JetLinksDataTypeCodecs.encode(buildValueType(dataType, random)).orElse(Collections.emptyMap());
excelInfo.setValueType(JSONObject.toJSONString(valueType));
excelInfo.setExpands(Collections.singletonMap("storageType", excelInfo.getStorageType()));
excelInfo.setType(Arrays.asList("read", "write", "report"));
return Flux.just(excelInfo);
}).doOnError(e -> {
log.error("填充模板异常:", e);
});
}
private static DataType buildValueType(DataType dataType, Random random) {
switch (dataType.getId()) {
case ArrayType.ID:
((ArrayType) dataType).elementType(new IntType().unit(idList.get(random.nextInt(idList.size() - 1))));
break;
case DoubleType.ID:
((DoubleType) dataType).scale(random.nextInt(10)).unit(idList.get(random.nextInt(idList.size() - 1)));
break;
case FloatType.ID:
((FloatType) dataType).scale(random.nextInt(10)).unit(idList.get(random.nextInt(idList.size() - 1)));
break;
case EnumType.ID:
dataType = new EnumType();
for (int i = 0; i < random.nextInt(5); i++) {
((EnumType) dataType).addElement(EnumType.Element.of("枚举值" + i, String.valueOf(i), "枚举说明" + i));
}
break;
case IntType.ID:
dataType = new IntType();
((IntType) dataType).unit(idList.get(random.nextInt(idList.size() - 1)));
break;
case LongType.ID:
((LongType) dataType).unit(idList.get(random.nextInt(idList.size() - 1)));
break;
case FileType.ID:
((FileType) dataType).bodyType(FileType.BodyType.url);
break;
case ObjectType.ID:
int i = 1;
List<String> objectParam = Lists.newCopyOnWriteArrayList(DATA_TYPES);
dataType = new ObjectType();
objectParam.removeAll(OBJECT_NOT_HAVE);
for (String id : objectParam) {
((ObjectType) dataType).addProperty("param" + i, "参数" + i, buildValueType(DataTypes.lookup(id).get(), random));
i++;
}
break;
case StringType.ID:
((StringType) dataType).expand("maxLength", random.nextInt(2000));
break;
case PasswordType.ID:
((PasswordType) dataType).expand("maxLength", random.nextInt(30));
break;
default:
break;
}
return dataType;
}
public Map<String, Object> toMap() {
setSource(PropertySource.getText(source));
setStorageType(PropertyStorage.getText(storageType));
setExpands(Collections.singletonMap("storageType", storageType));
Map<String, Object> map = FastBeanCopier.copy(this, new HashMap<>(8));
map.put("type", type.stream()
.map(PropertyType::getText)
.collect(Collectors.joining(",")));
return map;
}
@AllArgsConstructor
@Getter
private enum PropertySource implements EnumDict<String> {
device("设备"),
manual("手动"),
rule("规则");
private String text;
@Override
public String getValue() {
return name();
}
public static String getText(String value) {
return EnumDict.findByValue(PropertySource.class, value).map(PropertySource::getText).orElse("");
}
public static String getValue(String text) {
return EnumDict.findByText(PropertySource.class, text).map(PropertySource::getValue).orElse("");
}
}
@AllArgsConstructor
@Getter
private enum PropertyType implements EnumDict<String> {
read(""),
write(""),
report("上报");
private String text;
@Override
public String getValue() {
return name();
}
public static String getText(String value) {
return EnumDict.findByValue(PropertyType.class, value).map(PropertyType::getText).orElse("");
}
public static String getValue(String text) {
return EnumDict.findByText(PropertyType.class, text).map(PropertyType::getValue).orElse("");
}
}
@AllArgsConstructor
@Getter
private enum PropertyStorage implements EnumDict<String> {
direct("存储"),
ignore("不存储");
private String text;
@Override
public String getValue() {
return name();
}
public static String getText(String value) {
return EnumDict.findByValue(PropertyStorage.class, value).map(PropertyStorage::getText).orElse("");
}
public static String getValue(String text) {
return EnumDict.findByText(PropertyStorage.class, text).map(PropertyStorage::getValue).orElse("");
}
}
}

View File

@ -0,0 +1,56 @@
package org.jetlinks.community.device.web.excel;
import org.hswebframework.reactor.excel.Cell;
import org.hswebframework.reactor.excel.converter.RowWrapper;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.jetlinks.core.metadata.ConfigPropertyMetadata;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class PropertyMetadataWrapper extends RowWrapper<PropertyMetadataExcelInfo> {
private final Map<String, String> propertyMapping = new HashMap<>();
public PropertyMetadataWrapper(List<ConfigMetadata> expands) {
propertyMapping.put("属性ID", "property");
propertyMapping.put("属性名称", "name");
propertyMapping.put("数据类型", "dataType");
propertyMapping.put("单位", "unit");
propertyMapping.put("精度", "scale");
propertyMapping.put("数据类型配置", "valueType");
propertyMapping.put("来源", "source");
propertyMapping.put("属性说明", "description");
propertyMapping.put("读写类型", "type");
for (ConfigMetadata expand : expands) {
for (ConfigPropertyMetadata property : expand.getProperties()) {
propertyMapping.put(expand.getName() + "-" + property.getName(), property.getProperty());
propertyMapping.put(property.getName(), property.getProperty());
}
}
}
@Override
protected PropertyMetadataExcelInfo newInstance() {
return new PropertyMetadataExcelInfo();
}
@Override
protected PropertyMetadataExcelInfo wrap(PropertyMetadataExcelInfo instance, Cell header, Cell dataCell) {
String headerText = header.valueAsText().orElse("null");
Object value = dataCell.valueAsText().orElse("");
if (propertyMapping.containsKey(headerText)) {
instance.with(propertyMapping.get(headerText), propertyTypeToLowerCase(headerText, value));
}
instance.setRowNumber(dataCell.getRowIndex() + 1);
return instance;
}
private Object propertyTypeToLowerCase(String headerText, Object value) {
if ("类型".equals(headerText)) {
return value.toString().toLowerCase();
}
return value;
}
}

View File

@ -28,8 +28,8 @@ org.jetlinks.community.device.enums.TaskState.sendError=\u53D1\u9001\u5931\u8D25
org.jetlinks.community.device.enums.FirmwareUpgradeState.waiting=\u7B49\u5F85\u5347\u7EA7
org.jetlinks.community.device.enums.FirmwareUpgradeState.processing=\u5347\u7EA7\u4E2D
org.jetlinks.community.device.enums.FirmwareUpgradeState.failed=\u5347\u7EA7\u5931\u8D25
org.jetlinks.community.device.enums.FirmwareUpgradeState.success=\u5347\u7ea7\u5b8c\u6210
org.jetlinks.community.device.enums.FirmwareUpgradeState.canceled=\u5df2\u505c\u6b62
org.jetlinks.community.device.enums.FirmwareUpgradeState.success=\u5347\u7EA7\u5B8C\u6210
org.jetlinks.community.device.enums.FirmwareUpgradeState.canceled=\u5DF2\u505C\u6B62
#message
@ -69,7 +69,7 @@ error.product_does_not_exist=\u4EA7\u54C1{0}\u4E0D\u5B58\u5728
error.reply_is_error=\u8BBE\u5907\u54CD\u5E94\u9519\u8BEF
error.cannot_deleted_because_device_is_associated_with_it=\u5B58\u5728\u5173\u8054\u8BBE\u5907,\u65E0\u6CD5\u5220\u9664!
error.unable_to_load_protocol=\u65E0\u6CD5\u52A0\u8F7D\u534F\u8BAE:{0}
error.unable_to_load_protocol_by_access_id=\u627e\u4e0d\u5230\u5f53\u524d\u63a5\u5165\u65b9\u5f0f\u4e2d\u7684\u534f\u8bae:{0}
error.unable_to_load_protocol_by_access_id=\u627E\u4E0D\u5230\u5F53\u524D\u63A5\u5165\u65B9\u5F0F\u4E2D\u7684\u534F\u8BAE:{0}
error.unknown_data_type=\u672A\u77E5\u7684\u6570\u636E\u7C7B\u578B:{0}
error.unrecognized_message=\u65E0\u6CD5\u8BC6\u522B\u7684\u6D88\u606F
error.device_ID_already_exists=\u8BBE\u5907ID\u5DF2\u5B58\u5728
@ -81,7 +81,7 @@ error.product_ID_cannot_be_empty=\u4EA7\u54C1ID\u4E0D\u80FD\u4E3A\u7A7A
error.message_format=\u6D88\u606F\u683C\u5F0F\u9519\u8BEF
error.gateway_cannot_be_bound_as_a_child_device=\u4E0D\u80FD\u7ED1\u5B9A\u7F51\u5173\u81EA\u8EAB\u4E3A\u5B50\u8BBE\u5907
error.device_not_found_or_not_activated=\u8BBE\u5907\u4E0D\u5B58\u5728\u6216\u672A\u542F\u7528
error.device_category_has_bean_use_by_product=\u8BBE\u5907\u5206\u7C7B\u5DF2\u7ECF\u88AB\u5176\u4ED6\u4EA7\u54C1\u4F7F\u7528
error.device_category_has_bean_use_by_product=\u4EA7\u54C1\u5206\u7C7B\u5DF2\u7ECF\u88AB\u5176\u4ED6\u4EA7\u54C1\u4F7F\u7528
error.storage_policy_unsupported_operation=\u5B58\u50A8\u7B56\u7565\u4E0D\u652F\u6301\u6B64\u64CD\u4F5C
error.message_protocol_can_not_be_empty=\u6D88\u606F\u534F\u8BAE\u4E0D\u80FD\u4E3A\u7A7A
error.please_select_the_access_mode_first=\u8BF7\u5148\u9009\u62E9\u63A5\u5165\u65B9\u5F0F

View File

@ -0,0 +1,113 @@
package org.jetlinks.community.network.manager.service;
import lombok.AllArgsConstructor;
import org.hswebframework.web.crud.events.EntityBeforeDeleteEvent;
import org.hswebframework.web.crud.events.EntityCreatedEvent;
import org.hswebframework.web.crud.events.EntityModifyEvent;
import org.hswebframework.web.crud.events.EntitySavedEvent;
import org.hswebframework.web.exception.BusinessException;
import org.jetlinks.community.network.NetworkManager;
import org.jetlinks.community.network.NetworkProperties;
import org.jetlinks.community.network.manager.entity.CertificateEntity;
import org.jetlinks.community.network.manager.entity.NetworkConfigEntity;
import org.jetlinks.community.network.manager.enums.NetworkConfigState;
import org.jetlinks.community.reference.DataReferenceManager;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Collection;
@Component
@AllArgsConstructor
public class NetworkEntityEventHandler {
private final NetworkConfigService networkService;
private final DataReferenceManager referenceManager;
private final NetworkManager networkManager;
//禁止删除已有网络组件使用的证书
@EventListener
public void handleCertificateDelete(EntityBeforeDeleteEvent<CertificateEntity> event) {
event.async(
Flux.fromIterable(event.getEntity())
.flatMap(e -> networkService
.createQuery()
// FIXME: 2021/9/13 由于网络组件没有直接记录证书还有更好的处理办法
.$like$(NetworkConfigEntity::getConfiguration, e.getId())
.or()
.$like$(NetworkConfigEntity::getCluster, e.getId())
.count()
.doOnNext(i -> {
if (i > 0) {
throw new BusinessException("error.certificate_has_bean_use_by_network");
}
})
)
);
}
//禁止删除已有网关使用的网络组件
@EventListener
public void handleNetworkDelete(EntityBeforeDeleteEvent<NetworkConfigEntity> event) {
event.async(
Flux.fromIterable(event.getEntity())
.flatMap(e -> referenceManager.assertNotReferenced(DataReferenceManager.TYPE_NETWORK, e.getId()))
);
}
@EventListener
public void handleNetworkCreated(EntityCreatedEvent<NetworkConfigEntity> event) {
event.async(
Flux.fromIterable(event.getEntity())
.flatMapIterable(NetworkConfigEntity::toNetworkPropertiesList)
.flatMap(this::networkConfigValidate)
.then(handleEvent(event.getEntity()))
);
}
@EventListener
public void handleNetworkSaved(EntitySavedEvent<NetworkConfigEntity> event) {
event.async(
Flux.fromIterable(event.getEntity())
.filter(conf -> conf.getConfiguration() != null || conf.getCluster() != null)
.flatMapIterable(NetworkConfigEntity::toNetworkPropertiesList)
.flatMap(this::networkConfigValidate)
.then(handleEvent(event.getEntity()))
);
}
@EventListener
public void handleNetworkModify(EntityModifyEvent<NetworkConfigEntity> event) {
event.async(
Flux.fromIterable(event.getAfter())
.filter(conf -> conf.getConfiguration() != null || conf.getCluster() != null)
.flatMapIterable(NetworkConfigEntity::toNetworkPropertiesList)
.flatMap(this::networkConfigValidate)
.then(handleEvent(event.getAfter()))
);
}
//网络组件配置验证
private Mono<Void> networkConfigValidate(NetworkProperties properties) {
return Mono.justOrEmpty(networkManager.getProvider(properties.getType()))
.flatMap(networkProvider -> networkProvider.createConfig(properties))
.then();
}
private Mono<Void> handleEvent(Collection<NetworkConfigEntity> entities) {
return Flux
.fromIterable(entities)
.filter(conf -> conf.getState() == NetworkConfigState.enabled)
.flatMap(conf -> networkManager.reload(conf.lookupNetworkType(), conf.getId()))
.then();
}
}

View File

@ -0,0 +1,47 @@
package org.jetlinks.community.network.manager.service;
import lombok.AllArgsConstructor;
import org.jetlinks.community.network.manager.entity.DeviceGatewayEntity;
import org.jetlinks.community.reference.DataReferenceInfo;
import org.jetlinks.community.reference.DataReferenceManager;
import org.jetlinks.community.reference.DataReferenceProvider;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
/**
* 消息协议的数据引用提供商.
*
* 返回被设备接入网关使用的消息协议
*
* @author zhangji 2022/4/12
*/
@Component
@AllArgsConstructor
public class ProtocolDataReferenceProvider implements DataReferenceProvider {
private final DeviceGatewayService deviceGatewayService;
@Override
public String getId() {
return DataReferenceManager.TYPE_PROTOCOL;
}
@Override
public Flux<DataReferenceInfo> getReference(String protocolId) {
return deviceGatewayService
.createQuery()
.where()
.is(DeviceGatewayEntity::getProtocol, protocolId)
.fetch()
.map(e -> DataReferenceInfo.of(e.getId(),DataReferenceManager.TYPE_PROTOCOL, e.getProtocol(), e.getName()));
}
@Override
public Flux<DataReferenceInfo> getReferences() {
return deviceGatewayService
.createQuery()
.where()
.notNull(DeviceGatewayEntity::getChannelId)
.fetch()
.map(e -> DataReferenceInfo.of(e.getId(),DataReferenceManager.TYPE_PROTOCOL, e.getChannelId(), e.getName()));
}
}

View File

@ -76,6 +76,19 @@
<artifactId>notify-wechat</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>notify-webhook</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>notify-voice</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jetlinks.community</groupId>
<artifactId>notify-core</artifactId>

View File

@ -15,4 +15,14 @@ public class Notify {
private String dataId;
private long notifyTime;
private String code;
private Object detail;
public static Notify of(String message, String dataId, long timestamp) {
return new Notify(message, dataId, timestamp, null,null);
}
}

View File

@ -2,6 +2,8 @@ package org.jetlinks.community.notify.manager.subscriber;
import org.hswebframework.web.authorization.Authentication;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.jetlinks.core.metadata.PropertyMetadata;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Map;
@ -13,5 +15,9 @@ public interface SubscriberProvider {
Mono<Subscriber> createSubscriber(String id, Authentication authentication, Map<String, Object> config);
default Flux<PropertyMetadata> getDetailProperties(Map<String, Object> config) {
return Flux.empty();
}
ConfigMetadata getConfigMetadata();
}

View File

@ -0,0 +1,125 @@
package org.jetlinks.community.notify.manager.subscriber.providers;
import com.alibaba.fastjson.JSONObject;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.i18n.LocaleUtils;
import org.jetlinks.community.ValueObject;
import org.jetlinks.community.notify.manager.subscriber.Notify;
import org.jetlinks.community.notify.manager.subscriber.Subscriber;
import org.jetlinks.community.notify.manager.subscriber.SubscriberProvider;
import org.jetlinks.community.topic.Topics;
import org.jetlinks.core.event.EventBus;
import org.jetlinks.core.event.Subscription;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.jetlinks.core.metadata.DefaultConfigMetadata;
import org.jetlinks.core.metadata.PropertyMetadata;
import org.jetlinks.core.metadata.SimplePropertyMetadata;
import org.jetlinks.core.metadata.types.StringType;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.Objects;
@Component
public class AlarmProvider implements SubscriberProvider {
private final EventBus eventBus;
public AlarmProvider(EventBus eventBus) {
this.eventBus = eventBus;
}
@Override
public String getId() {
return "alarm";
}
@Override
public String getName() {
return "告警";
}
@Override
public ConfigMetadata getConfigMetadata() {
return new DefaultConfigMetadata()
.add("alarmConfigId", "告警规则", "告警规则,支持通配符:*", StringType.GLOBAL);
}
@Override
public Mono<Subscriber> createSubscriber(String id, Authentication authentication, Map<String, Object> config) {
ValueObject configs = ValueObject.of(config);
String alarmId = configs.getString("alarmConfigId").orElse("*");
String topic = Topics.alarm("*", "*", alarmId);
return Mono.justOrEmpty(()-> createSubscribe(id, new String[]{topic}));
}
private Flux<Notify> createSubscribe(String id,
String[] topics) {
Subscription.Feature[] features = new Subscription.Feature[]{Subscription.Feature.local};
return Flux
.defer(() -> this
.eventBus
.subscribe(Subscription.of("alarm:" + id, topics, features))
.map(msg -> {
JSONObject json = msg.bodyToJson();
return Notify.of(
getNotifyMessage(json),
//告警记录ID
json.getString("id"),
System.currentTimeMillis(),
"alarm",
json
);
}));
}
private static String getNotifyMessage(JSONObject json) {
String message;
TargetType targetType = TargetType.of(json.getString("targetType"));
String targetName = json.getString("targetName");
String alarmName = json.getString("alarmName");
if (targetType == TargetType.other) {
message = String.format("[%s]发生告警:[%s]!", targetName, alarmName);
} else {
message = String.format("%s[%s]发生告警:[%s]!", targetType.getText(), targetName, alarmName);
}
return LocaleUtils.resolveMessage("message.alarm.notify." + targetType.name(), message, targetName, alarmName);
}
@Override
public Flux<PropertyMetadata> getDetailProperties(Map<String, Object> config) {
//todo 根据配置来获取输出数据
return Flux.just(
SimplePropertyMetadata.of("targetType", "告警类型", StringType.GLOBAL),
SimplePropertyMetadata.of("alarmName", "告警名称", StringType.GLOBAL),
SimplePropertyMetadata.of("targetName", "目标名称", StringType.GLOBAL)
);
}
@AllArgsConstructor
@Getter
enum TargetType {
device("设备"),
product("产品"),
other("其它");
private final String text;
public static TargetType of(String name) {
for (TargetType value : TargetType.values()) {
if (Objects.equals(value.name(), name)) {
return value;
}
}
return TargetType.other;
}
}
}

View File

@ -1,74 +0,0 @@
package org.jetlinks.community.notify.manager.subscriber.providers;
import com.alibaba.fastjson.JSONObject;
import org.hswebframework.web.authorization.Authentication;
import org.jetlinks.community.ValueObject;
import org.jetlinks.community.notify.manager.subscriber.Notify;
import org.jetlinks.community.notify.manager.subscriber.Subscriber;
import org.jetlinks.community.notify.manager.subscriber.SubscriberProvider;
import org.jetlinks.core.event.EventBus;
import org.jetlinks.core.event.Subscription;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.jetlinks.core.metadata.DefaultConfigMetadata;
import org.jetlinks.core.metadata.types.StringType;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Map;
@Component
public class DeviceAlarmProvider implements SubscriberProvider {
private final EventBus eventBus;
public DeviceAlarmProvider(EventBus eventBus) {
this.eventBus = eventBus;
}
@Override
public String getId() {
return "device_alarm";
}
@Override
public String getName() {
return "设备告警";
}
@Override
public ConfigMetadata getConfigMetadata() {
return new DefaultConfigMetadata()
.add("productId", "产品ID", "产品ID,支持通配符:*", StringType.GLOBAL)
.add("deviceId", "设备ID", "设备ID,支持通配符:*", StringType.GLOBAL)
.add("productId", "告警ID", "告警ID,支持通配符:*", StringType.GLOBAL)
;
}
@Override
public Mono<Subscriber> createSubscriber(String id, Authentication authentication, Map<String, Object> config) {
ValueObject configs = ValueObject.of(config);
String productId = configs.getString("productId").orElse("*");
String deviceId = configs.getString("deviceId").orElse("*");
String alarmId = configs.getString("alarmId").orElse("*");
Flux<Notify> flux = eventBus
.subscribe(Subscription.of("device-alarm:" + id,
String.format("/rule-engine/device/alarm/%s/%s/%s", productId, deviceId, alarmId),
Subscription.Feature.local
))
.map(msg -> {
JSONObject json = msg.bodyToJson(true);
return Notify.of(
String.format("设备[%s]发生告警:[%s]!", json.getString("deviceName"), json.getString("alarmName")),
json.getString("alarmId"),
System.currentTimeMillis()
);
});
return Mono.just(() -> flux);
}
}

View File

@ -4,15 +4,16 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.web.authorization.annotation.Resource;
import org.hswebframework.web.authorization.annotation.ResourceAction;
import org.hswebframework.web.exception.NotFoundException;
import org.jetlinks.community.notify.DefaultNotifyType;
import org.jetlinks.community.notify.NotifierManager;
import org.jetlinks.community.notify.NotifyType;
import org.jetlinks.community.notify.manager.entity.NotifyTemplateEntity;
import org.jetlinks.community.notify.manager.service.NotifyConfigService;
import org.jetlinks.community.notify.template.TemplateManager;
import org.jetlinks.core.Values;
import org.springframework.web.bind.annotation.*;
@ -27,17 +28,15 @@ import java.util.function.Function;
@RequestMapping("/notifier")
@Resource(id = "notifier", name = "通知管理")
@Tag(name = "消息通知管理")
@AllArgsConstructor
public class NotifierController {
private final NotifyConfigService configService;
private final NotifierManager notifierManager;
private final TemplateManager templateManager;
public NotifierController(NotifierManager notifierManager, TemplateManager templateManager) {
this.notifierManager = notifierManager;
this.templateManager = templateManager;
}
/**
* 指定通知器以及模版.发送通知.
@ -52,16 +51,37 @@ public class NotifierController {
public Mono<Void> sendNotify(@PathVariable @Parameter(description = "通知配置ID") String notifierId,
@RequestBody Mono<SendNotifyRequest> mono) {
return mono.flatMap(tem -> {
NotifyType type = DefaultNotifyType.valueOf(tem.getTemplate().getType());
return Mono.zip(
notifierManager.getNotifier(type, notifierId)
.switchIfEmpty(Mono.error(() -> new NotFoundException("通知器[" + notifierId + "]不存在"))),
templateManager.createTemplate(type, tem.getTemplate().toTemplateProperties()),
(notifier, template) -> notifier.send(template, Values.of(tem.getContext())))
NotifyType type = NotifyType.of(tem.getTemplate().getType());
return Mono
.zip(
notifierManager
.getNotifier(type, notifierId)
.switchIfEmpty(Mono.error(() -> new NotFoundException("error.notifier_does_not_exist", notifierId))),
templateManager.createTemplate(type, tem.getTemplate().toTemplateProperties()),
(notifier, template) -> notifier.send(template, Values.of(tem.getContext())))
.flatMap(Function.identity());
});
}
@PostMapping("/{notifierId}/{templateId}/_send")
@ResourceAction(id = "send", name = "发送通知")
@Operation(summary = "根据配置和模版ID发送消息通知")
public Mono<Void> sendNotify(@PathVariable @Parameter(description = "通知配置ID") String notifierId,
@PathVariable @Parameter(description = "通知模版ID") String templateId,
@RequestBody Mono<Map<String, Object>> contextMono) {
return configService
.findById(notifierId)
.flatMap(conf -> Mono
.zip(
notifierManager
.getNotifier(NotifyType.of(conf.getType()), notifierId)
.switchIfEmpty(Mono.error(() -> new NotFoundException("error.notifier_does_not_exist", notifierId))),
contextMono,
(notifier, contextMap) -> notifier.send(templateId, Values.of(contextMap))
)
.flatMap(Function.identity()));
}
@Getter
@Setter
public static class SendNotifyRequest {
@ -74,4 +94,4 @@ public class NotifierController {
private Map<String, Object> context = new HashMap<>();
}
}
}

View File

@ -1,23 +1,26 @@
package org.jetlinks.community.notify.manager.web;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.hswebframework.web.api.crud.entity.QueryParamEntity;
import org.hswebframework.web.authorization.annotation.Authorize;
import org.hswebframework.web.authorization.annotation.QueryAction;
import org.hswebframework.web.authorization.annotation.Resource;
import org.hswebframework.web.crud.service.ReactiveCrudService;
import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
import org.hswebframework.web.exception.ValidationException;
import org.jetlinks.community.notify.manager.entity.NotifyTemplateEntity;
import org.jetlinks.community.notify.manager.service.NotifyConfigService;
import org.jetlinks.community.notify.manager.service.NotifyTemplateService;
import org.jetlinks.community.notify.manager.web.response.TemplateInfo;
import org.jetlinks.community.notify.template.TemplateProvider;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
/**
@ -35,11 +38,14 @@ public class NotifierTemplateController implements ReactiveServiceCrudController
private final NotifyTemplateService templateService;
private final List<TemplateProvider> providers;
private final NotifyConfigService configService;
public NotifierTemplateController(NotifyTemplateService templateService, List<TemplateProvider> providers) {
public NotifierTemplateController(NotifyTemplateService templateService,
NotifyConfigService configService,
List<TemplateProvider> providers) {
this.templateService = templateService;
this.providers = providers;
this.configService = configService;
}
@Override
@ -47,17 +53,69 @@ public class NotifierTemplateController implements ReactiveServiceCrudController
return templateService;
}
@PostMapping("/{configId}/_query")
@QueryAction
@Operation(summary = "根据配置ID查询通知模版列表")
public Flux<NotifyTemplateEntity> queryTemplatesByConfigId(@PathVariable
@Parameter(description = "配置ID") String configId,
@RequestBody Mono<QueryParamEntity> query) {
return configService
.findById(configId)
.flatMapMany(conf -> query
.flatMapMany(param -> param
.toNestQuery(nest -> nest
//where type = ? and provider = ? and (config_id = ? or config_id is null or config_id = '')
.is(NotifyTemplateEntity::getType, conf.getType())
.is(NotifyTemplateEntity::getProvider, conf.getProvider())
.nest()
/**/.is(NotifyTemplateEntity::getConfigId, configId)
/* */.or()
/**/.isNull(NotifyTemplateEntity::getConfigId)
.isEmpty(NotifyTemplateEntity::getConfigId)
)
.noPaging()
.execute(templateService::query)));
}
@GetMapping("/{templateId}/detail")
@QueryAction
@Operation(summary = "获取模版详情信息")
public Mono<TemplateInfo> getTemplateDetail(@PathVariable
@Parameter(description = "模版ID") String templateId) {
return templateService
.findById(templateId)
.flatMap(e -> {
TemplateInfo info = new TemplateInfo();
info.setId(e.getId());
info.setName(e.getName());
return this
.getProvider(e.getType(), e.getProvider())
.createTemplate(e.toTemplateProperties())
.doOnNext(t -> info.setVariableDefinitions(new ArrayList<>(t.getVariables().values())))
.thenReturn(info);
});
}
@GetMapping("/{type}/{provider}/config/metadata")
@QueryAction
@Operation(summary = "获取指定类型和服务商所需模版配置定义")
public Mono<ConfigMetadata> getAllTypes(@PathVariable String type,
@PathVariable String provider) {
return Flux.fromIterable(providers)
.filter(prov -> prov.getType().getId().equalsIgnoreCase(type) && prov.getProvider().getId().equalsIgnoreCase(provider))
.flatMap(prov -> Mono.justOrEmpty(prov.getTemplateConfigMetadata()))
.next();
public Mono<ConfigMetadata> getConfigMetadata(@PathVariable @Parameter(description = "通知类型ID") String type,
@PathVariable @Parameter(description = "服务商ID") String provider) {
return Mono.justOrEmpty(getProvider(type, provider).getTemplateConfigMetadata());
}
public TemplateProvider getProvider(String type, String provider) {
for (TemplateProvider prov : providers) {
if (prov.getType().getId().equalsIgnoreCase(type) && prov
.getProvider()
.getId()
.equalsIgnoreCase(provider)) {
return prov;
}
}
throw new ValidationException("error.unsupported_notify_provider");
}
}

View File

@ -0,0 +1,28 @@
package org.jetlinks.community.notify.manager.web.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetlinks.community.notify.template.VariableDefinition;
import java.util.List;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class TemplateInfo {
@Schema(description = "模版ID")
private String id;
@Schema(description = "模版名称")
private String name;
@Schema(description = "变量定义信息")
private List<VariableDefinition> variableDefinitions;
}

View File

@ -1,11 +1,16 @@
package org.jetlinks.community.rule.engine.configuration;
import org.jetlinks.community.elastic.search.index.ElasticSearchIndexManager;
import org.jetlinks.community.elastic.search.service.ElasticSearchService;
import org.jetlinks.community.rule.engine.scene.SceneFilter;
import org.jetlinks.community.rule.engine.scene.SceneTaskExecutorProvider;
import org.jetlinks.community.rule.engine.service.ElasticSearchAlarmHistoryService;
import org.jetlinks.core.event.EventBus;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@AutoConfiguration
public class RuleEngineManagerConfiguration {
@ -17,4 +22,15 @@ public class RuleEngineManagerConfiguration {
return new SceneTaskExecutorProvider(eventBus,
SceneFilter.composite(filters));
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ElasticSearchService.class)
static class ElasticSearchAlarmHistoryConfiguration {
@Bean(initMethod = "init")
public ElasticSearchAlarmHistoryService alarmHistoryService(ElasticSearchService elasticSearchService,
ElasticSearchIndexManager indexManager) {
return new ElasticSearchAlarmHistoryService(indexManager, elasticSearchService);
}
}
}

View File

@ -0,0 +1,21 @@
package org.jetlinks.community.rule.engine.measurement;
import lombok.AllArgsConstructor;
import lombok.Generated;
import lombok.Getter;
import org.jetlinks.community.dashboard.DashboardDefinition;
/**
* @author bestfeng
*/
@Getter
@AllArgsConstructor
@Generated
public enum AlarmDashboardDefinition implements DashboardDefinition {
alarm("alarm","告警信息");
private String id;
private String name;
}

View File

@ -0,0 +1,21 @@
package org.jetlinks.community.rule.engine.measurement;
import lombok.AllArgsConstructor;
import lombok.Generated;
import lombok.Getter;
import org.jetlinks.community.dashboard.ObjectDefinition;
@Getter
@AllArgsConstructor
@Generated
public enum AlarmObjectDefinition implements ObjectDefinition {
record("告警记录");
@Override
public String getId() {
return name();
}
private String name;
}

View File

@ -0,0 +1,51 @@
package org.jetlinks.community.rule.engine.measurement;
import com.google.common.collect.Maps;
import io.micrometer.core.instrument.MeterRegistry;
import org.jetlinks.community.PropertyConstants;
import org.jetlinks.community.dashboard.supports.StaticMeasurementProvider;
import org.jetlinks.community.micrometer.MeterRegistryManager;
import org.jetlinks.community.rule.engine.entity.AlarmHistoryInfo;
import org.jetlinks.community.timeseries.TimeSeriesManager;
import org.jetlinks.community.utils.ConverterUtils;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author bestfeng
*/
@Component
public class AlarmRecordMeasurementProvider extends StaticMeasurementProvider {
MeterRegistry registry;
public AlarmRecordMeasurementProvider(MeterRegistryManager registryManager,
TimeSeriesManager timeSeriesManager) {
super(AlarmDashboardDefinition.alarm, AlarmObjectDefinition.record);
registry = registryManager.getMeterRegister(AlarmTimeSeriesMetric.alarmStreamMetrics().getId());
addMeasurement(new AlarmRecordTrendMeasurement(timeSeriesManager));
addMeasurement(new AlarmRecordRankMeasurement(timeSeriesManager));
}
@EventListener
public void aggAlarmRecord(AlarmHistoryInfo info) {
registry
.counter("record-agg", getTags(info))
.increment();
}
public String[] getTags(AlarmHistoryInfo info) {
Map<String, Object> tagMap = Maps.newLinkedHashMap();
tagMap.put("targetId", info.getTargetId());
tagMap.put("targetType", info.getTargetType());
tagMap.put("targetName", info.getTargetName());
tagMap.put("alarmConfigId", info.getAlarmConfigId());
return ConverterUtils.convertMapToTags(tagMap);
}
}

View File

@ -0,0 +1,135 @@
package org.jetlinks.community.rule.engine.measurement;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetlinks.community.dashboard.*;
import org.jetlinks.community.dashboard.supports.StaticMeasurement;
import org.jetlinks.community.timeseries.TimeSeriesManager;
import org.jetlinks.community.timeseries.query.Aggregation;
import org.jetlinks.community.timeseries.query.AggregationData;
import org.jetlinks.community.timeseries.query.AggregationQueryParam;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.jetlinks.core.metadata.DataType;
import org.jetlinks.core.metadata.DefaultConfigMetadata;
import org.jetlinks.core.metadata.types.IntType;
import org.jetlinks.core.metadata.types.StringType;
import reactor.core.publisher.Flux;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Comparator;
import java.util.Date;
import java.util.Objects;
/**
* @author bestfeng
*/
public class AlarmRecordRankMeasurement extends StaticMeasurement {
TimeSeriesManager timeSeriesManager;
public AlarmRecordRankMeasurement(TimeSeriesManager timeSeriesManager) {
super(MeasurementDefinition.of("rank", "告警记录排名"));
this.timeSeriesManager = timeSeriesManager;
addDimension(new AggRecordRankDimension());
}
static ConfigMetadata aggConfigMetadata = new DefaultConfigMetadata()
.add("time", "周期", "例如: 1h,10m,30s", StringType.GLOBAL)
.add("agg", "聚合类型", "count,sum,avg,max,min", StringType.GLOBAL)
.add("format", "时间格式", "如: MM-dd:HH", StringType.GLOBAL)
.add("limit", "最大数据量", "", StringType.GLOBAL)
.add("from", "时间从", "", StringType.GLOBAL)
.add("to", "时间至", "", StringType.GLOBAL);
class AggRecordRankDimension implements MeasurementDimension {
@Override
public DimensionDefinition getDefinition() {
return CommonDimensionDefinition.agg;
}
@Override
public DataType getValueType() {
return IntType.GLOBAL;
}
@Override
public ConfigMetadata getParams() {
return aggConfigMetadata;
}
@Override
public boolean isRealTime() {
return false;
}
public AggregationQueryParam createQueryParam(MeasurementParameter parameter) {
return AggregationQueryParam
.of()
.groupBy(parameter.getString("group", "targetId"))
.sum("count", "count")
.agg("targetId", Aggregation.TOP)
.filter(query -> query
.where("name", "record-agg")
.where("targetType", parameter.getString("targetType", null))
)
.limit(parameter.getInt("limit").orElse(1))
.from(parameter
.getDate("from")
.orElseGet(() -> Date
.from(LocalDateTime
.now()
.plusDays(-1)
.atZone(ZoneId.systemDefault())
.toInstant())))
.to(parameter.getDate("to").orElse(new Date()));
}
@Override
public Flux<SimpleMeasurementValue> getValue(MeasurementParameter parameter) {
Comparator<AggregationData> comparator;
if (Objects.equals(parameter.getString("order",""), "asc")){
comparator = Comparator.comparingInt(d-> d.getInt("count", 0));
}else {
comparator = Comparator.<AggregationData>comparingInt(d-> d.getInt("count", 0)).reversed();
}
AggregationQueryParam param = createQueryParam(parameter);
return Flux.defer(() -> param
.execute(timeSeriesManager.getService(AlarmTimeSeriesMetric.alarmStreamMetrics())::aggregation)
.groupBy(a -> a.getString("targetId", null))
.flatMap(fluxGroup -> fluxGroup.reduce(AggregationData::merge))
.sort(comparator)
.map(data -> SimpleMeasurementValue.of(new SimpleResult(data), 0))
)
.take(param.getLimit());
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
class SimpleResult {
private String targetId;
private String targetName;
private Integer count;
public SimpleResult(AggregationData data) {
String targetId = data.getString("targetId", "");
this.setCount(data.getInt("count", 0));
this.setTargetName(data.getString("targetName", targetId));
this.setTargetId(data.getString("targetId", ""));
}
}
}
}

View File

@ -0,0 +1,101 @@
package org.jetlinks.community.rule.engine.measurement;
import org.jetlinks.community.dashboard.*;
import org.jetlinks.community.dashboard.supports.StaticMeasurement;
import org.jetlinks.community.timeseries.TimeSeriesManager;
import org.jetlinks.community.timeseries.query.AggregationQueryParam;
import org.jetlinks.core.metadata.ConfigMetadata;
import org.jetlinks.core.metadata.DataType;
import org.jetlinks.core.metadata.DefaultConfigMetadata;
import org.jetlinks.core.metadata.types.IntType;
import org.jetlinks.core.metadata.types.StringType;
import reactor.core.publisher.Flux;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
/**
* @author bestfeng
*/
public class AlarmRecordTrendMeasurement extends StaticMeasurement {
TimeSeriesManager timeSeriesManager;
public AlarmRecordTrendMeasurement(TimeSeriesManager timeSeriesManager) {
super(MeasurementDefinition.of("trend", "告警记录趋势"));
this.timeSeriesManager = timeSeriesManager;
addDimension(new AggRecordTrendDimension());
}
static ConfigMetadata aggConfigMetadata = new DefaultConfigMetadata()
.add("alarmConfigId", "告警配置Id", "", StringType.GLOBAL)
.add("time", "周期", "例如: 1h,10m,30s", StringType.GLOBAL)
.add("agg", "聚合类型", "count,sum,avg,max,min", StringType.GLOBAL)
.add("format", "时间格式", "如: MM-dd:HH", StringType.GLOBAL)
.add("limit", "最大数据量", "", StringType.GLOBAL)
.add("from", "时间从", "", StringType.GLOBAL)
.add("to", "时间至", "", StringType.GLOBAL);
class AggRecordTrendDimension implements MeasurementDimension{
@Override
public DimensionDefinition getDefinition() {
return CommonDimensionDefinition.agg;
}
@Override
public DataType getValueType() {
return IntType.GLOBAL;
}
@Override
public ConfigMetadata getParams() {
return aggConfigMetadata;
}
@Override
public boolean isRealTime() {
return false;
}
public AggregationQueryParam createQueryParam(MeasurementParameter parameter) {
return AggregationQueryParam
.of()
.groupBy(parameter.getInterval("time", null),
parameter.getString("format").orElse("MM月dd日 HH时"))
.sum("count", "count")
.filter(query -> query
.where("name", "record-agg")
.and("targetType",parameter.getString("targetType").orElse(null))
.and("targetId",parameter.getString("targetId").orElse(null))
.is("alarmConfigId", parameter.getString("alarmConfigId").orElse(null))
)
.limit(parameter.getInt("limit").orElse(1))
.from(parameter
.getDate("from")
.orElseGet(() -> Date
.from(LocalDateTime
.now()
.plusDays(-1)
.atZone(ZoneId.systemDefault())
.toInstant())))
.to(parameter.getDate("to").orElse(new Date()));
}
@Override
public Flux<SimpleMeasurementValue> getValue(MeasurementParameter parameter) {
AggregationQueryParam param = createQueryParam(parameter);
return Flux.defer(()-> param
.execute(timeSeriesManager.getService(AlarmTimeSeriesMetric.alarmStreamMetrics())::aggregation)
.index((index, data) -> SimpleMeasurementValue.of(
data.getLong("count",0),
data.getString("time",""),
index)))
.take(param.getLimit());
}
}
}

View File

@ -0,0 +1,23 @@
package org.jetlinks.community.rule.engine.measurement;
import org.jetlinks.community.timeseries.TimeSeriesMetric;
/**
* 媒体时序数据度量标识
*
* @author bestfeng
*
* @see org.jetlinks.pro.timeseries.TimeSeriesService
* @see TimeSeriesMetric
*/
public interface AlarmTimeSeriesMetric {
/**
* 告警监控指标,用于对告警进行进行监控
*
* @return 度量标识
*/
static TimeSeriesMetric alarmStreamMetrics() {
return TimeSeriesMetric.of("alarm_metrics");
}
}

View File

@ -16,6 +16,7 @@ import org.hswebframework.web.i18n.LocaleUtils;
import org.hswebframework.web.validator.ValidatorUtils;
import org.jetlinks.core.device.DeviceRegistry;
import org.jetlinks.core.metadata.DeviceMetadata;
import org.jetlinks.core.utils.Reactors;
import org.jetlinks.community.TimerSpec;
import org.jetlinks.community.rule.engine.executor.DeviceMessageSendTaskExecutorProvider;
import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorSpec;
@ -24,9 +25,12 @@ import org.jetlinks.community.rule.engine.scene.term.TermColumn;
import org.jetlinks.community.rule.engine.scene.term.TermTypeSupport;
import org.jetlinks.community.rule.engine.scene.term.TermTypes;
import org.jetlinks.community.rule.engine.scene.value.TermValue;
import org.jetlinks.reactor.ql.ReactorQL;
import org.jetlinks.reactor.ql.ReactorQLContext;
import org.jetlinks.rule.engine.api.model.RuleModel;
import org.jetlinks.rule.engine.api.model.RuleNodeModel;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -53,6 +57,10 @@ public class DeviceTrigger extends DeviceSelectorSpec implements Serializable {
private DeviceOperation operation;
public SqlRequest createSql(List<Term> terms) {
return createSql(terms, true);
}
public SqlRequest createSql(List<Term> terms, boolean hasWhere) {
Map<String, Term> termsMap = SceneUtils.expandTerm(terms);
// select * from (
@ -78,6 +86,8 @@ public class DeviceTrigger extends DeviceSelectorSpec implements Serializable {
selectColumns.add("this.headers._uid \"_uid\"");
//维度绑定信息,如部门等
selectColumns.add("this.headers.bindings \"_bindings\"");
//链路追踪ID
selectColumns.add("this.headers.traceparent \"traceparent\"");
switch (this.operation.getOperator()) {
case readProperty:
@ -133,14 +143,56 @@ public class DeviceTrigger extends DeviceSelectorSpec implements Serializable {
builder.append("\t\nfrom ").append(createFromTable());
builder.append("\n) t \n");
SqlFragments fragments = terms == null ? EmptySqlFragments.INSTANCE : termBuilder.createTermFragments(this, terms);
if (!fragments.isEmpty()) {
SqlRequest request = fragments.toRequest();
builder.append("where ").append(request.getSql());
if (hasWhere) {
SqlFragments fragments = terms == null ? EmptySqlFragments.INSTANCE : termBuilder.createTermFragments(this, terms);
if (!fragments.isEmpty()) {
SqlRequest request = fragments.toRequest();
builder.append("where ").append(request.getSql());
}
return PrepareSqlRequest.of(builder.toString(), fragments.getParameters().toArray());
}
return PrepareSqlRequest.of(builder.toString(), fragments.getParameters().toArray());
return PrepareSqlRequest.of(builder.toString(), new Object[0]);
}
String createFilterDescription(List<Term> terms) {
SqlFragments fragments = CollectionUtils.isEmpty(terms) ? EmptySqlFragments.INSTANCE : termBuilder.createTermFragments(this, terms);
return fragments.isEmpty() ? "true" : fragments.toRequest().toNativeSql();
}
Function<Map<String, Object>, Mono<Boolean>> createFilter(List<Term> terms) {
SqlFragments fragments = CollectionUtils.isEmpty(terms) ? EmptySqlFragments.INSTANCE : termBuilder.createTermFragments(this, terms);
if (!fragments.isEmpty()) {
SqlRequest request = fragments.toRequest();
String sql = "select 1 from t where " + request.getSql();
ReactorQL ql = ReactorQL
.builder()
.sql(sql)
.build();
Object[] args = request.getParameters();
String sqlString = request.toNativeSql();
return new Function<Map<String, Object>, Mono<Boolean>>() {
@Override
public Mono<Boolean> apply(Map<String, Object> map) {
ReactorQLContext context = ReactorQLContext.ofDatasource((t) -> Flux.just(map));
for (Object arg : args) {
context.bind(arg);
}
return ql
.start(context)
.hasElements();
}
@Override
public String toString() {
return sqlString;
}
};
}
return ignore -> Reactors.ALWAYS_TRUE;
}
@ -363,6 +415,8 @@ public class DeviceTrigger extends DeviceSelectorSpec implements Serializable {
timerNode.setId("scene:device:timer");
timerNode.setName("定时下发指令");
timerNode.setExecutor("timer");
//使用最小负载节点来执行定时
// timerNode.setSchedulingRule(SchedulerSelectorStrategy.minimumLoad());
timerNode.setConfiguration(FastBeanCopier.copy(timer, new HashMap<>()));
model.getNodes().add(timerNode);

View File

@ -0,0 +1,21 @@
package org.jetlinks.community.rule.engine.scene;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.List;
@Getter
@Setter
public class SceneActions implements Serializable {
@Schema(description = "是否并行执行动作")
private boolean parallel;
@Schema(description = "执行动作")
private List<SceneAction> actions;
}

View File

@ -0,0 +1,25 @@
package org.jetlinks.community.rule.engine.scene;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.ezorm.core.param.Term;
import org.jetlinks.community.rule.engine.commons.ShakeLimit;
import java.io.Serializable;
import java.util.List;
@Getter
@Setter
public class SceneConditionAction implements Serializable {
@Schema(description = "条件")
private List<Term> when;
@Schema(description = "防抖配置")
private ShakeLimit shakeLimit;
@Schema(description = "满足条件时执行的动作")
private SceneActions then;
}

View File

@ -13,21 +13,28 @@ import org.hswebframework.web.i18n.LocaleUtils;
import org.hswebframework.web.validator.ValidatorUtils;
import org.jetlinks.core.device.DeviceRegistry;
import org.jetlinks.core.metadata.types.DateTimeType;
import org.jetlinks.core.utils.Reactors;
import org.jetlinks.community.rule.engine.commons.ShakeLimit;
import org.jetlinks.community.rule.engine.commons.TermsConditionEvaluator;
import org.jetlinks.community.rule.engine.scene.term.TermColumn;
import org.jetlinks.community.rule.engine.scene.term.limit.ShakeLimitGrouping;
import org.jetlinks.rule.engine.api.model.RuleLink;
import org.jetlinks.rule.engine.api.model.RuleModel;
import org.jetlinks.rule.engine.api.model.RuleNodeModel;
import org.jetlinks.rule.engine.defaults.AbstractExecutionContext;
import reactor.core.Disposable;
import reactor.core.Disposables;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import reactor.util.concurrent.Queues;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
@Getter
@Setter
@ -45,11 +52,6 @@ public class SceneRule implements Serializable {
@NotNull(message = "error.scene_rule_trigger_cannot_be_null")
private Trigger trigger;
/**
* @see TermColumn
* @see org.jetlinks.community.rule.engine.scene.term.TermType
* @see org.jetlinks.community.rule.engine.scene.value.TermValue
*/
@Schema(description = "触发条件")
private List<Term> terms;
@ -59,17 +61,42 @@ public class SceneRule implements Serializable {
@Schema(description = "执行动作")
private List<SceneAction> actions;
@Schema(description = "动作分支")
private List<SceneConditionAction> branches;
@Schema(description = "说明")
private String description;
public SqlRequest createSql() {
public SqlRequest createSql(boolean hasWhere) {
if (trigger != null && trigger.getType() == TriggerType.device) {
return trigger.getDevice().createSql(terms);
return trigger.getDevice().createSql(terms, hasWhere);
}
return EmptySqlRequest.INSTANCE;
}
public Function<Map<String, Object>, Mono<Boolean>> createFilter(List<Term> terms) {
if (trigger != null && trigger.getType() == TriggerType.device) {
return trigger.getDevice().createFilter(terms);
}
return ignore -> Reactors.ALWAYS_TRUE;
}
String createFilterDescription(List<Term> terms) {
if (trigger != null && trigger.getType() == TriggerType.device) {
return trigger.getDevice().createFilterDescription(terms);
}
return "true";
}
public ShakeLimitGrouping<Map<String, Object>> createGrouping() {
//todo 其他分组方式实现
return flux -> flux
.groupBy(map -> map.getOrDefault("deviceId", "null"), Integer.MAX_VALUE);
}
private Flux<Variable> createSceneVariables(List<TermColumn> columns) {
return LocaleUtils
.currentReactive()
@ -87,8 +114,8 @@ public class SceneRule implements Serializable {
List<Variable> termVar = SceneUtils.parseVariable(terms, columns);
List<Variable> variables = new ArrayList<>(defaultVariables.size() + termVar.size());
//设备触发但是没有指定条件,以下是内置的输出参数
if (CollectionUtils.isEmpty(termVar) && trigger.getType() == TriggerType.device) {
//设备触发但是没有指定条件,或者其它触发类型,以下是内置的输出参数
if (trigger.getType() != TriggerType.device || CollectionUtils.isEmpty(termVar)) {
variables.add(Variable
.of("_now",
LocaleUtils.resolveMessage(
@ -113,22 +140,153 @@ public class SceneRule implements Serializable {
}
public Flux<Variable> createVariables(List<TermColumn> columns,
Integer branchIndex,
Integer actionIndex,
DeviceRegistry registry) {
Flux<Variable> variables = createSceneVariables(columns);
//执行动作会输出的变量,串行执行才会生效
if (!parallel && actionIndex != null && CollectionUtils.isNotEmpty(actions)) {
if (branchIndex == null && !parallel && actionIndex != null && CollectionUtils.isNotEmpty(actions)) {
for (int i = 0; i < Math.min(actions.size(), actionIndex + 1); i++) {
variables = variables.concatWith(actions.get(i).createVariables(registry, i));
}
}
//分支条件
if (branchIndex != null && CollectionUtils.isNotEmpty(branches) && branches.size() > branchIndex) {
SceneConditionAction branch = branches.get(branchIndex);
List<SceneAction> actionList;
if (branch.getThen() != null && !branch.getThen().isParallel() &&
CollectionUtils.isNotEmpty(actionList = branch.getThen().getActions())) {
for (int i = 0; i < Math.min(actionList.size(), actionIndex + 1); i++) {
variables = variables.concatWith(actionList.get(i).createVariables(registry, i));
}
}
}
return variables
.doOnNext(Variable::refactorPrefix);
}
public Disposable createBranchHandler(Flux<Map<String, Object>> sourceData,
BiFunction<String, Map<String, Object>, Mono<Void>> output) {
if (CollectionUtils.isEmpty(branches)) {
return Disposables.disposed();
}
Function<Map<String, Object>, Mono<Boolean>> last = null;
Disposable.Composite disposable = Disposables.composite();
int branchIndex = 0;
for (SceneConditionAction branch : branches) {
int _branchIndex = ++branchIndex;
//执行条件
Function<Map<String, Object>, Mono<Boolean>> filter = createFilter(branch.getWhen());
//满足条件后的输出操作
Function<Map<String, Object>, Mono<Void>> out;
SceneActions then = branch.getThen();
//执行动作
if (then != null && CollectionUtils.isNotEmpty(then.getActions())) {
int size = then.getActions().size();
//串行只传递到第一个动作
if (!then.isParallel() || size == 1) {
String nodeId = "branch_" + _branchIndex + "_action_1";
out = data -> output.apply(nodeId, data);
} else {
//多个并行执行动作
String[] nodeIds = new String[size];
for (int i = 0; i < nodeIds.length; i++) {
nodeIds[0] = "branch_" + _branchIndex + "_action_" + (i + 1);
}
Flux<String> nodeIdFlux = Flux.fromArray(nodeIds);
//并行
out = data -> nodeIdFlux
.flatMap(nodeId -> output.apply(nodeId, data))
.then();
}
//防抖
ShakeLimit shakeLimit = branch.getShakeLimit();
if (shakeLimit != null && shakeLimit.isEnabled()) {
Sinks.Many<Map<String, Object>> sinks = Sinks
.many()
.unicast()
.onBackpressureBuffer(Queues.<Map<String, Object>>unboundedMultiproducer().get());
//分组方式,比如设备触发时,应该按设备分组,每个设备都走独立的防抖策略
ShakeLimitGrouping<Map<String, Object>> grouping = createGrouping();
Function<Map<String, Object>, Mono<Void>> handler = out;
disposable.add(
shakeLimit
.transfer(sinks.asFlux(),
(duration, stream) ->
grouping
.group(stream)//先按自定义分组再按事件窗口进行分组
.flatMap(group -> group.window(duration), Integer.MAX_VALUE),
(map, total) -> map.put("_total", total))
.flatMap(handler)
.subscribe()
);
//输出到sink进行防抖控制
out = data -> {
sinks.emitNext(data, Reactors.emitFailureHandler());
return Mono.empty();
};
}
} else {
out = ignore -> Mono.empty();
}
Function<Map<String, Object>, Mono<Void>> fOut = out;
Function<Map<String, Object>, Mono<Boolean>> handler =
data -> filter
.apply(data)
.flatMap(match -> {
// 满足条件后执行输出
if (match) {
return fOut.apply(data).thenReturn(true);
}
return Reactors.ALWAYS_FALSE;
});
if (last == null) {
last = handler;
} else {
Function<Map<String, Object>, Mono<Boolean>> _last = last;
last = data -> _last
.apply(data)
.flatMap(match -> {
//上一个分支满足了则返回,不执行此分支逻辑
if (match) {
return Reactors.ALWAYS_FALSE;
}
return handler.apply(data);
});
}
}
//never happen
if (last == null) {
disposable.dispose();
throw new IllegalArgumentException();
}
disposable.add(
sourceData.flatMap(last).subscribe()
);
return disposable;
}
public List<Variable> createDefaultVariable() {
return trigger != null
? trigger.createDefaultVariable()
@ -199,6 +357,49 @@ public class SceneRule implements Serializable {
}
}
//使用分支条件时
if (CollectionUtils.isNotEmpty(branches)) {
int branchIndex = 0;
for (SceneConditionAction branch : branches) {
branchIndex++;
SceneActions actions = branch.getThen();
if (actions != null && CollectionUtils.isNotEmpty(actions.getActions())) {
int actionIndex = 1;
RuleNodeModel preNode = null;
SceneAction preAction = null;
for (SceneAction action : actions.getActions()) {
RuleNodeModel actionNode = new RuleNodeModel();
actionNode.setId("branch_" + branchIndex + "_action_" + actionIndex);
actionNode.setName("条件_" + branchIndex + "_动作_" + actionIndex);
action.applyNode(actionNode);
//串行
if (!actions.isParallel()) {
//串行的时候 标记记录每一个动作的数据到header中用于进行条件判断或者数据引用
actionNode.addConfiguration(AbstractExecutionContext.RECORD_DATA_TO_HEADER, true);
actionNode.addConfiguration(AbstractExecutionContext.RECORD_DATA_TO_HEADER_KEY, actionNode.getId());
if (preNode != null) {
//上一个节点->当前动作节点
RuleLink link = model.link(preNode, actionNode);
//设置上一个节点到此节点的输出条件
if (CollectionUtils.isNotEmpty(preAction.getTerms())) {
link.setCondition(TermsConditionEvaluator.createCondition(preAction.getTerms()));
}
}
preNode = actionNode;
}
model.getNodes().add(actionNode);
preAction = action;
actionIndex++;
}
}
}
}
return model;
}

View File

@ -2,6 +2,7 @@ package org.jetlinks.community.rule.engine.scene;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.hswebframework.ezorm.rdb.executor.SqlRequest;
import org.hswebframework.web.bean.FastBeanCopier;
import org.hswebframework.web.id.IDGenerator;
@ -89,7 +90,9 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider {
if (disposable != null) {
disposable.dispose();
}
SqlRequest request = rule.createSql();
boolean useBranch = CollectionUtils.isNotEmpty(rule.getBranches());
SqlRequest request = rule.createSql(!useBranch);
//不是通过SQL来处理数据
if (request.isEmpty()) {

View File

@ -3,15 +3,13 @@ package org.jetlinks.community.rule.engine.web;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
import org.hswebframework.web.authorization.annotation.Authorize;
import org.hswebframework.web.authorization.annotation.QueryAction;
import org.hswebframework.web.authorization.annotation.Resource;
import org.hswebframework.web.authorization.annotation.SaveAction;
import org.hswebframework.web.crud.service.ReactiveCrudService;
import org.hswebframework.web.crud.web.reactive.ReactiveServiceQueryController;
import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
import org.jetlinks.community.rule.engine.alarm.AlarmLevelInfo;
import org.jetlinks.community.rule.engine.alarm.AlarmTargetSupplier;
import org.jetlinks.community.rule.engine.entity.AlarmConfigEntity;
@ -29,8 +27,7 @@ import reactor.core.publisher.Mono;
@Authorize
@Tag(name = "告警配置")
@AllArgsConstructor
public class AlarmConfigController implements ReactiveServiceQueryController<AlarmConfigEntity, String> {
public class AlarmConfigController implements ReactiveServiceCrudController<AlarmConfigEntity, String> {
private final AlarmConfigService alarmConfigService;
private final ReactiveRepository<AlarmLevelEntity, String> alarmLevelRepository;
@ -89,5 +86,4 @@ public class AlarmConfigController implements ReactiveServiceQueryController<Ala
public Mono<AlarmLevelEntity> queryAlarmLevel() {
return alarmLevelRepository.findById(AlarmLevelService.DEFAULT_ALARM_ID);
}
}

View File

@ -110,6 +110,7 @@ public class SceneController implements ReactiveServiceQueryController<SceneEnti
@Operation(summary = "解析规则中输出的变量")
@QueryAction
public Flux<Variable> parseVariables(@RequestBody Mono<SceneRule> ruleMono,
@RequestParam(required = false) Integer branch,
@RequestParam(required = false) Integer action) {
Mono<SceneRule> cache = ruleMono.cache();
return Mono
@ -123,6 +124,7 @@ public class SceneController implements ReactiveServiceQueryController<SceneEnti
.filter(column -> column.hasColumn(terms.keySet()))
.map(column -> column.copyColumn(terms::containsKey))
.collect(Collectors.toList()),
branch,
action,
deviceRegistry);
})

View File

@ -0,0 +1,191 @@
server:
port: 8850
spring:
redis:
host: 127.0.0.1
port: 6380
lettuce:
pool:
max-active: 1024
timeout: 20s
serializer: jdk # 设置fst时,redis key使用string序列化,value使用 fst序列化.
# database: 3
# max-wait: 10s
r2dbc:
# 需要手动创建数据库,启动会自动创建表,修改了配置easyorm相关配置也要修改
url: r2dbc:postgresql://localhost:5433/jetlinks
# url: r2dbc:mysql://localhost:3306/jetlinks?ssl=false&serverZoneId=Asia/Shanghai # 修改了配置easyorm相关配置也要修改
username: postgres
password: jetlinks
pool:
max-size: 32
max-idle-time: 2m # 值不能大于mysql server的wait_timeout配置
max-life-time: 10m
acquire-retry: 3
reactor:
debug-agent:
enabled: false
elasticsearch:
uris: localhost:9201
socket-timeout: 10s
connection-timeout: 15s
webclient:
max-in-memory-size: 100MB
easyorm:
default-schema: public # 数据库默认的schema
dialect: postgres #数据库方言
elasticsearch:
embedded:
enabled: false # 为true时使用内嵌的elasticsearch,不建议在生产环境中使用
data-path: ./data/elasticsearch
port: 9201
host: 0.0.0.0
index:
default-strategy: time-by-month #默认es的索引按月进行分表, direct则为直接操作索引.
settings:
number-of-shards: 1 # es 分片数量
number-of-replicas: 0 # 副本数量
device:
message:
writer:
time-series:
enabled: true #写出设备消息数据到elasticsearch
captcha:
enabled: false # 开启验证码
ttl: 2m #验证码过期时间,2分钟
hsweb:
cors:
enable: true
configs:
- path: /**
allowed-headers: "*"
allowed-methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
allowed-origins: ["*"]
# allow-credentials: true
max-age: 1800
dict:
enum-packages: org.jetlinks
file:
upload:
static-file-path: ./static/upload
static-location: http://192.168.32.65:8850/upload
webflux:
response-wrapper:
enabled: true #开启响应包装器(将返回值包装为ResponseMessage)
excludes: # 这下包下的接口不包装
- org.springdoc
# auth: #默认的用户配置
# users:
# admin:
# username: admin
# password: admin
# name: 超级管理员
authorize:
auto-parse: true
permission:
filter:
enabled: true # 设置为true开启权限过滤,赋权时,不能赋予比自己多的权限.
exclude-username: admin # admin用户不受上述限制
un-auth-strategy: ignore # error表示:发生越权时,抛出403错误. ignore表示会忽略越权的赋权.
cache:
type: none
redis:
local-cache-type: guava
file:
manager:
storage-base-path: ./data/files
api:
# 访问api接口的根地址
base-path: http://127.0.0.1:${server.port}
jetlinks:
server-id: ${spring.application.name}:${server.port} #设备服务网关服务ID,不同服务请设置不同的ID
logging:
system:
context:
server: ${spring.application.name}
protocol:
spi:
enabled: true # 为true时开启自动加载通过依赖引入的协议包
logging:
level:
org.jetlinks: debug
rule.engine: debug
org.hswebframework: debug
org.springframework.transaction: debug
org.springframework.data.r2dbc.connectionfactory: warn
io.micrometer: warn
org.hswebframework.expands: error
system: debug
org.jetlinks.rule.engine: warn
org.jetlinks.supports.event: warn
org.springframework: warn
org.jetlinks.community.device.message.writer: warn
org.jetlinks.community.timeseries.micrometer: warn
org.jetlinks.community.elastic.search.service.reactive: trace
org.jetlinks.community.network: warn
io.vertx.mqtt.impl: warn
org.jetlinks.supports.scalecube.rpc: warn
"org.jetlinks.community.buffer": debug
# org.springframework.data.elasticsearch.client: trace
# org.elasticsearch: error
org.elasticsearch: error
org.elasticsearch.deprecation.search.aggregations.bucket.histogram: error
config: classpath:logback-spring.xml
vertx:
max-event-loop-execute-time-unit: seconds
max-event-loop-execute-time: 30
max-worker-execute-time-unit: seconds
max-worker-execute-time: 30
prefer-native-transport: true
micrometer:
time-series:
tags:
server: ${spring.application.name}
metrics:
default:
step: 30s
management:
health:
elasticsearch:
enabled: false # 关闭elasticsearch健康检查
springdoc:
swagger-ui:
path: /swagger-ui.html
# packages-to-scan: org.jetlinks
group-configs:
- group: 设备管理相关接口
packages-to-scan:
- org.jetlinks.community.device
paths-to-exclude:
- /device-instance/**
- /device-product/**
- /protocol/**
- group: 规则引擎相关接口
packages-to-scan: org.jetlinks.community.rule.engine.web
paths-to-exclude: /api/**
- group: 通知管理相关接口
packages-to-scan: org.jetlinks.community.notify.manager.web
- group: 设备接入相关接口
packages-to-scan:
- org.jetlinks.community.network.manager.web
- org.jetlinks.community.device.web
paths-to-match:
- /gateway/**
- /network/**
- /protocol/**
- group: 系统管理相关接口
packages-to-scan:
- org.jetlinks.community.auth
- org.hswebframework.web.system.authorization.defaults.webflux
- org.hswebframework.web.file
- org.hswebframework.web.authorization.basic.web
- org.jetlinks.community.logging.controller
cache:
disabled: false
network:
resources:
- 2883-2890
- 18800-18810
- 15060-15061