同步2.0相关功能代码 (#206)
This commit is contained in:
parent
1a25e693b9
commit
f408d72159
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
message.device_message_handing=消息已发往设备,处理中...
|
||||
|
||||
error.data.referenced=数据已经被其他地方使用
|
||||
error.duplicate_key_detail=重复的数据:{0}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
org.jetlinks.community.things.configuration.ThingsConfiguration
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ public class DeviceInstanceImportExportEntity {
|
|||
@ExcelProperty("设备名称")
|
||||
private String name;
|
||||
|
||||
@ExcelProperty("设备型号")
|
||||
@ExcelProperty("产品名称")
|
||||
private String productName;
|
||||
|
||||
@ExcelProperty("描述")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
})
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
769
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/LocalDeviceInstanceService.java
Normal file → Executable file
769
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/LocalDeviceInstanceService.java
Normal file → Executable 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
/**
|
||||
* 注册设备物模型信息
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
44
jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierController.java
Normal file → Executable file
44
jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierController.java
Normal file → Executable 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<>();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue