feat:【IoT 物联网】新增场景规则触发器匹配策略接口和实现
This commit is contained in:
parent
0d288077d3
commit
49bf744b74
@ -63,4 +63,18 @@ public enum IotSceneRuleTriggerTypeEnum implements ArrayValuable<Integer> {
|
|||||||
return ARRAYS;
|
return ARRAYS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型值查找触发器类型枚举
|
||||||
|
*
|
||||||
|
* @param typeValue 类型值
|
||||||
|
* @return 触发器类型枚举
|
||||||
|
*/
|
||||||
|
public static IotSceneRuleTriggerTypeEnum findTriggerTypeEnum(Integer typeValue) {
|
||||||
|
return Arrays.stream(IotSceneRuleTriggerTypeEnum.values())
|
||||||
|
.filter(type -> type.getType().equals(typeValue))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,122 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.NumberUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
|
||||||
|
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||||
|
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||||
|
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoT 场景规则触发器匹配器抽象基类
|
||||||
|
* <p>
|
||||||
|
* 提供通用的条件评估逻辑和工具方法
|
||||||
|
*
|
||||||
|
* @author HUIHUI
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public abstract class AbstractIotSceneRuleTriggerMatcher implements IotSceneRuleTriggerMatcher {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估条件是否匹配
|
||||||
|
*
|
||||||
|
* @param sourceValue 源值(来自消息)
|
||||||
|
* @param operator 操作符
|
||||||
|
* @param paramValue 参数值(来自条件配置)
|
||||||
|
* @return 是否匹配
|
||||||
|
*/
|
||||||
|
protected boolean evaluateCondition(Object sourceValue, String operator, String paramValue) {
|
||||||
|
try {
|
||||||
|
// 1. 校验操作符是否合法
|
||||||
|
IotSceneRuleConditionOperatorEnum operatorEnum = IotSceneRuleConditionOperatorEnum.operatorOf(operator);
|
||||||
|
if (operatorEnum == null) {
|
||||||
|
log.warn("[evaluateCondition][存在错误的操作符({})]", operator);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 构建 Spring 表达式变量
|
||||||
|
Map<String, Object> springExpressionVariables = new HashMap<>();
|
||||||
|
springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, sourceValue);
|
||||||
|
|
||||||
|
// 处理参数值
|
||||||
|
if (StrUtil.isNotBlank(paramValue)) {
|
||||||
|
// 处理多值情况(如 IN、BETWEEN 操作符)
|
||||||
|
if (paramValue.contains(",")) {
|
||||||
|
List<String> paramValues = StrUtil.split(paramValue, ',');
|
||||||
|
springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE_LIST,
|
||||||
|
convertList(paramValues, NumberUtil::parseDouble));
|
||||||
|
} else {
|
||||||
|
// 处理单值情况
|
||||||
|
springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE,
|
||||||
|
NumberUtil.parseDouble(paramValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 计算 Spring 表达式
|
||||||
|
return (Boolean) SpringExpressionUtils.parseExpression(operatorEnum.getSpringExpression(), springExpressionVariables);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[evaluateCondition][条件评估异常] sourceValue: {}, operator: {}, paramValue: {}",
|
||||||
|
sourceValue, operator, paramValue, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查基础触发器参数是否有效
|
||||||
|
*
|
||||||
|
* @param trigger 触发器配置
|
||||||
|
* @return 是否有效
|
||||||
|
*/
|
||||||
|
protected boolean isBasicTriggerValid(IotSceneRuleDO.Trigger trigger) {
|
||||||
|
return trigger != null && trigger.getType() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查操作符和值是否有效
|
||||||
|
*
|
||||||
|
* @param trigger 触发器配置
|
||||||
|
* @return 是否有效
|
||||||
|
*/
|
||||||
|
protected boolean isOperatorAndValueValid(IotSceneRuleDO.Trigger trigger) {
|
||||||
|
return StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查标识符是否匹配
|
||||||
|
*
|
||||||
|
* @param expectedIdentifier 期望的标识符
|
||||||
|
* @param actualIdentifier 实际的标识符
|
||||||
|
* @return 是否匹配
|
||||||
|
*/
|
||||||
|
protected boolean isIdentifierMatched(String expectedIdentifier, String actualIdentifier) {
|
||||||
|
return StrUtil.isNotBlank(expectedIdentifier) && expectedIdentifier.equals(actualIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录匹配成功日志
|
||||||
|
*
|
||||||
|
* @param message 设备消息
|
||||||
|
* @param trigger 触发器配置
|
||||||
|
*/
|
||||||
|
protected void logMatchSuccess(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
|
||||||
|
log.debug("[{}][消息({}) 匹配触发器({}) 成功]", getMatcherName(), message.getRequestId(), trigger.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录匹配失败日志
|
||||||
|
*
|
||||||
|
* @param message 设备消息
|
||||||
|
* @param trigger 触发器配置
|
||||||
|
* @param reason 失败原因
|
||||||
|
*/
|
||||||
|
protected void logMatchFailure(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, String reason) {
|
||||||
|
log.debug("[{}][消息({}) 匹配触发器({}) 失败: {}]", getMatcherName(), message.getRequestId(), trigger.getType(), reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||||
|
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||||
|
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||||
|
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备事件上报触发器匹配器
|
||||||
|
* <p>
|
||||||
|
* 处理设备事件上报的触发器匹配逻辑
|
||||||
|
*
|
||||||
|
* @author HUIHUI
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class DeviceEventPostTriggerMatcher extends AbstractIotSceneRuleTriggerMatcher {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备事件上报消息方法
|
||||||
|
*/
|
||||||
|
private static final String DEVICE_EVENT_POST_METHOD = "thing.event.post";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
|
||||||
|
return IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
|
||||||
|
// 1. 基础参数校验
|
||||||
|
if (!isBasicTriggerValid(trigger)) {
|
||||||
|
logMatchFailure(message, trigger, "触发器基础参数无效");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查消息方法是否匹配
|
||||||
|
if (!DEVICE_EVENT_POST_METHOD.equals(message.getMethod())) {
|
||||||
|
logMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_EVENT_POST_METHOD + ", 实际: " + message.getMethod());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查标识符是否匹配
|
||||||
|
String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message);
|
||||||
|
if (!isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) {
|
||||||
|
logMatchFailure(message, trigger, "标识符不匹配,期望: " + trigger.getIdentifier() + ", 实际: " + messageIdentifier);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 对于事件触发器,通常不需要检查操作符和值,只要事件发生即匹配
|
||||||
|
// 但如果配置了操作符和值,则需要进行条件匹配
|
||||||
|
if (StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue())) {
|
||||||
|
Object eventData = message.getData();
|
||||||
|
if (eventData == null) {
|
||||||
|
logMatchFailure(message, trigger, "消息中事件数据为空");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean matched = evaluateCondition(eventData, trigger.getOperator(), trigger.getValue());
|
||||||
|
if (!matched) {
|
||||||
|
logMatchFailure(message, trigger, "事件数据条件不匹配");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logMatchSuccess(message, trigger);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 30; // 中等优先级
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||||
|
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||||
|
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||||
|
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备属性上报触发器匹配器
|
||||||
|
* <p>
|
||||||
|
* 处理设备属性数据上报的触发器匹配逻辑
|
||||||
|
*
|
||||||
|
* @author HUIHUI
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class DevicePropertyPostTriggerMatcher extends AbstractIotSceneRuleTriggerMatcher {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备属性上报消息方法
|
||||||
|
*/
|
||||||
|
private static final String DEVICE_PROPERTY_POST_METHOD = "thing.property.post";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
|
||||||
|
return IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
|
||||||
|
// 1. 基础参数校验
|
||||||
|
if (!isBasicTriggerValid(trigger)) {
|
||||||
|
logMatchFailure(message, trigger, "触发器基础参数无效");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查消息方法是否匹配
|
||||||
|
if (!DEVICE_PROPERTY_POST_METHOD.equals(message.getMethod())) {
|
||||||
|
logMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_PROPERTY_POST_METHOD + ", 实际: " + message.getMethod());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查标识符是否匹配
|
||||||
|
String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message);
|
||||||
|
if (!isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) {
|
||||||
|
logMatchFailure(message, trigger, "标识符不匹配,期望: " + trigger.getIdentifier() + ", 实际: " + messageIdentifier);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 检查操作符和值是否有效
|
||||||
|
if (!isOperatorAndValueValid(trigger)) {
|
||||||
|
logMatchFailure(message, trigger, "操作符或值无效");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 获取属性值
|
||||||
|
Object propertyValue = message.getData();
|
||||||
|
if (propertyValue == null) {
|
||||||
|
logMatchFailure(message, trigger, "消息中属性值为空");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 使用条件评估器进行匹配
|
||||||
|
boolean matched = evaluateCondition(propertyValue, trigger.getOperator(), trigger.getValue());
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
logMatchSuccess(message, trigger);
|
||||||
|
} else {
|
||||||
|
logMatchFailure(message, trigger, "属性值条件不匹配");
|
||||||
|
}
|
||||||
|
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 20; // 中等优先级
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||||
|
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
|
||||||
|
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||||
|
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备服务调用触发器匹配器
|
||||||
|
* <p>
|
||||||
|
* 处理设备服务调用的触发器匹配逻辑
|
||||||
|
*
|
||||||
|
* @author HUIHUI
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class DeviceServiceInvokeTriggerMatcher extends AbstractIotSceneRuleTriggerMatcher {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备服务调用消息方法
|
||||||
|
*/
|
||||||
|
private static final String DEVICE_SERVICE_INVOKE_METHOD = "thing.service.invoke";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
|
||||||
|
return IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
|
||||||
|
// 1. 基础参数校验
|
||||||
|
if (!isBasicTriggerValid(trigger)) {
|
||||||
|
logMatchFailure(message, trigger, "触发器基础参数无效");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查消息方法是否匹配
|
||||||
|
if (!DEVICE_SERVICE_INVOKE_METHOD.equals(message.getMethod())) {
|
||||||
|
logMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_SERVICE_INVOKE_METHOD + ", 实际: " + message.getMethod());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查标识符是否匹配
|
||||||
|
String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message);
|
||||||
|
if (!isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) {
|
||||||
|
logMatchFailure(message, trigger, "标识符不匹配,期望: " + trigger.getIdentifier() + ", 实际: " + messageIdentifier);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 对于服务调用触发器,通常只需要匹配服务标识符即可
|
||||||
|
// 不需要检查操作符和值,因为服务调用本身就是触发条件
|
||||||
|
|
||||||
|
logMatchSuccess(message, trigger);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 40; // 较低优先级
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||||
|
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||||
|
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备状态更新触发器匹配器
|
||||||
|
* <p>
|
||||||
|
* 处理设备上下线状态变更的触发器匹配逻辑
|
||||||
|
*
|
||||||
|
* @author HUIHUI
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class DeviceStateUpdateTriggerMatcher extends AbstractIotSceneRuleTriggerMatcher {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备状态更新消息方法
|
||||||
|
*/
|
||||||
|
private static final String DEVICE_STATE_UPDATE_METHOD = "thing.state.update";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
|
||||||
|
return IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
|
||||||
|
// 1. 基础参数校验
|
||||||
|
if (!isBasicTriggerValid(trigger)) {
|
||||||
|
logMatchFailure(message, trigger, "触发器基础参数无效");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查消息方法是否匹配
|
||||||
|
if (!DEVICE_STATE_UPDATE_METHOD.equals(message.getMethod())) {
|
||||||
|
logMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_STATE_UPDATE_METHOD + ", 实际: " + message.getMethod());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查操作符和值是否有效
|
||||||
|
if (!isOperatorAndValueValid(trigger)) {
|
||||||
|
logMatchFailure(message, trigger, "操作符或值无效");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 获取设备状态值
|
||||||
|
Object stateValue = message.getData();
|
||||||
|
if (stateValue == null) {
|
||||||
|
logMatchFailure(message, trigger, "消息中设备状态值为空");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 使用条件评估器进行匹配
|
||||||
|
boolean matched = evaluateCondition(stateValue, trigger.getOperator(), trigger.getValue());
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
logMatchSuccess(message, trigger);
|
||||||
|
} else {
|
||||||
|
logMatchFailure(message, trigger, "状态值条件不匹配");
|
||||||
|
}
|
||||||
|
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 10; // 高优先级
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||||
|
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||||
|
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoT 场景规则触发器匹配策略接口
|
||||||
|
* <p>
|
||||||
|
* 用于实现不同类型触发器的匹配逻辑,遵循策略模式设计
|
||||||
|
*
|
||||||
|
* @author HUIHUI
|
||||||
|
*/
|
||||||
|
public interface IotSceneRuleTriggerMatcher {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支持的触发器类型
|
||||||
|
*
|
||||||
|
* @return 触发器类型枚举
|
||||||
|
*/
|
||||||
|
IotSceneRuleTriggerTypeEnum getSupportedTriggerType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查触发器是否匹配消息
|
||||||
|
*
|
||||||
|
* @param message 设备消息
|
||||||
|
* @param trigger 触发器配置
|
||||||
|
* @return 是否匹配
|
||||||
|
*/
|
||||||
|
boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取匹配优先级(数值越小优先级越高)
|
||||||
|
* <p>
|
||||||
|
* 用于在多个匹配器支持同一触发器类型时确定优先级
|
||||||
|
*
|
||||||
|
* @return 优先级数值
|
||||||
|
*/
|
||||||
|
default int getPriority() {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取匹配器名称,用于日志和调试
|
||||||
|
*
|
||||||
|
* @return 匹配器名称
|
||||||
|
*/
|
||||||
|
default String getMatcherName() {
|
||||||
|
return this.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用该匹配器
|
||||||
|
* <p>
|
||||||
|
* 可用于动态开关某些匹配器
|
||||||
|
*
|
||||||
|
* @return 是否启用
|
||||||
|
*/
|
||||||
|
default boolean isEnabled() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,150 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||||
|
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||||
|
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum.findTriggerTypeEnum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoT 场景规则触发器匹配管理器
|
||||||
|
* <p>
|
||||||
|
* 负责管理所有触发器匹配器,并提供统一的匹配入口
|
||||||
|
*
|
||||||
|
* @author HUIHUI
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class IotSceneRuleTriggerMatcherManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发器匹配器映射表
|
||||||
|
* Key: 触发器类型枚举
|
||||||
|
* Value: 对应的匹配器实例
|
||||||
|
*/
|
||||||
|
private final Map<IotSceneRuleTriggerTypeEnum, IotSceneRuleTriggerMatcher> matcherMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有匹配器列表(按优先级排序)
|
||||||
|
*/
|
||||||
|
private final List<IotSceneRuleTriggerMatcher> allMatchers;
|
||||||
|
|
||||||
|
public IotSceneRuleTriggerMatcherManager(List<IotSceneRuleTriggerMatcher> matchers) {
|
||||||
|
if (CollUtil.isEmpty(matchers)) {
|
||||||
|
log.warn("[IotSceneRuleTriggerMatcherManager][没有找到任何触发器匹配器]");
|
||||||
|
this.matcherMap = new HashMap<>();
|
||||||
|
this.allMatchers = new ArrayList<>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按优先级排序并过滤启用的匹配器
|
||||||
|
this.allMatchers = matchers.stream()
|
||||||
|
.filter(IotSceneRuleTriggerMatcher::isEnabled)
|
||||||
|
.sorted(Comparator.comparing(IotSceneRuleTriggerMatcher::getPriority))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 构建匹配器映射表
|
||||||
|
this.matcherMap = this.allMatchers.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
IotSceneRuleTriggerMatcher::getSupportedTriggerType,
|
||||||
|
Function.identity(),
|
||||||
|
(existing, replacement) -> {
|
||||||
|
log.warn("[IotSceneRuleTriggerMatcherManager][触发器类型({})存在多个匹配器,使用优先级更高的: {}]",
|
||||||
|
existing.getSupportedTriggerType(),
|
||||||
|
existing.getPriority() <= replacement.getPriority() ? existing.getMatcherName() : replacement.getMatcherName());
|
||||||
|
return existing.getPriority() <= replacement.getPriority() ? existing : replacement;
|
||||||
|
},
|
||||||
|
LinkedHashMap::new
|
||||||
|
));
|
||||||
|
|
||||||
|
log.info("[IotSceneRuleTriggerMatcherManager][初始化完成,共加载 {} 个触发器匹配器]", this.matcherMap.size());
|
||||||
|
this.matcherMap.forEach((type, matcher) ->
|
||||||
|
log.info("[IotSceneRuleTriggerMatcherManager][触发器类型: {}, 匹配器: {}, 优先级: {}]",
|
||||||
|
type, matcher.getMatcherName(), matcher.getPriority()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查触发器是否匹配消息
|
||||||
|
*
|
||||||
|
* @param message 设备消息
|
||||||
|
* @param trigger 触发器配置
|
||||||
|
* @return 是否匹配
|
||||||
|
*/
|
||||||
|
public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
|
||||||
|
if (message == null || trigger == null || trigger.getType() == null) {
|
||||||
|
log.debug("[isMatched][参数无效] message: {}, trigger: {}", message, trigger);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据触发器类型查找对应的匹配器
|
||||||
|
IotSceneRuleTriggerTypeEnum triggerType = findTriggerTypeEnum(trigger.getType());
|
||||||
|
if (triggerType == null) {
|
||||||
|
log.warn("[isMatched][未知的触发器类型: {}]", trigger.getType());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IotSceneRuleTriggerMatcher matcher = matcherMap.get(triggerType);
|
||||||
|
if (matcher == null) {
|
||||||
|
log.warn("[isMatched][触发器类型({})没有对应的匹配器]", triggerType);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return matcher.isMatched(message, trigger);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[isMatched][触发器匹配异常] message: {}, trigger: {}, matcher: {}",
|
||||||
|
message, trigger, matcher.getMatcherName(), e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有支持的触发器类型
|
||||||
|
*
|
||||||
|
* @return 支持的触发器类型列表
|
||||||
|
*/
|
||||||
|
public Set<IotSceneRuleTriggerTypeEnum> getSupportedTriggerTypes() {
|
||||||
|
return new HashSet<>(matcherMap.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定触发器类型的匹配器
|
||||||
|
*
|
||||||
|
* @param triggerType 触发器类型
|
||||||
|
* @return 匹配器实例,如果不存在则返回 null
|
||||||
|
*/
|
||||||
|
public IotSceneRuleTriggerMatcher getMatcher(IotSceneRuleTriggerTypeEnum triggerType) {
|
||||||
|
return matcherMap.get(triggerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有匹配器的统计信息
|
||||||
|
*
|
||||||
|
* @return 统计信息映射表
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getMatcherStatistics() {
|
||||||
|
Map<String, Object> statistics = new HashMap<>();
|
||||||
|
statistics.put("totalMatchers", allMatchers.size());
|
||||||
|
statistics.put("enabledMatchers", matcherMap.size());
|
||||||
|
statistics.put("supportedTriggerTypes", getSupportedTriggerTypes());
|
||||||
|
|
||||||
|
Map<String, Object> matcherDetails = new HashMap<>();
|
||||||
|
matcherMap.forEach((type, matcher) -> {
|
||||||
|
Map<String, Object> detail = new HashMap<>();
|
||||||
|
detail.put("matcherName", matcher.getMatcherName());
|
||||||
|
detail.put("priority", matcher.getPriority());
|
||||||
|
detail.put("enabled", matcher.isEnabled());
|
||||||
|
matcherDetails.put(type.name(), detail);
|
||||||
|
});
|
||||||
|
statistics.put("matcherDetails", matcherDetails);
|
||||||
|
|
||||||
|
return statistics;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||||
|
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||||
|
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时触发器匹配器
|
||||||
|
* <p>
|
||||||
|
* 处理定时触发的触发器匹配逻辑
|
||||||
|
* 注意:定时触发器不依赖设备消息,主要用于定时任务场景
|
||||||
|
*
|
||||||
|
* @author HUIHUI
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class TimerTriggerMatcher extends AbstractIotSceneRuleTriggerMatcher {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
|
||||||
|
return IotSceneRuleTriggerTypeEnum.TIMER;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
|
||||||
|
// 1. 基础参数校验
|
||||||
|
if (!isBasicTriggerValid(trigger)) {
|
||||||
|
logMatchFailure(message, trigger, "触发器基础参数无效");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查 CRON 表达式是否存在
|
||||||
|
if (StrUtil.isBlank(trigger.getCronExpression())) {
|
||||||
|
logMatchFailure(message, trigger, "定时触发器缺少 CRON 表达式");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 定时触发器通常不依赖具体的设备消息
|
||||||
|
// 它是通过定时任务调度器触发的,这里主要是验证配置的有效性
|
||||||
|
|
||||||
|
// 4. 可以添加 CRON 表达式格式验证
|
||||||
|
if (!isValidCronExpression(trigger.getCronExpression())) {
|
||||||
|
logMatchFailure(message, trigger, "CRON 表达式格式无效: " + trigger.getCronExpression());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logMatchSuccess(message, trigger);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 CRON 表达式格式是否有效
|
||||||
|
*
|
||||||
|
* @param cronExpression CRON 表达式
|
||||||
|
* @return 是否有效
|
||||||
|
*/
|
||||||
|
private boolean isValidCronExpression(String cronExpression) {
|
||||||
|
try {
|
||||||
|
// 简单的 CRON 表达式格式验证
|
||||||
|
// 标准 CRON 表达式应该有 6 或 7 个字段(秒 分 时 日 月 周 [年])
|
||||||
|
String[] fields = cronExpression.trim().split("\\s+");
|
||||||
|
return fields.length >= 6 && fields.length <= 7;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 50; // 最低优先级,因为定时触发器不依赖消息
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
// 定时触发器可以根据配置动态启用/禁用
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,200 @@
|
|||||||
|
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||||
|
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
|
||||||
|
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
|
||||||
|
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoT 场景规则触发器匹配器测试类
|
||||||
|
*
|
||||||
|
* @author HUIHUI
|
||||||
|
*/
|
||||||
|
public class IotSceneRuleTriggerMatcherTest extends BaseMockitoUnitTest {
|
||||||
|
|
||||||
|
private IotSceneRuleTriggerMatcherManager matcherManager;
|
||||||
|
private List<IotSceneRuleTriggerMatcher> matchers;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// 创建所有匹配器实例
|
||||||
|
matchers = Arrays.asList(
|
||||||
|
new DeviceStateUpdateTriggerMatcher(),
|
||||||
|
new DevicePropertyPostTriggerMatcher(),
|
||||||
|
new DeviceEventPostTriggerMatcher(),
|
||||||
|
new DeviceServiceInvokeTriggerMatcher(),
|
||||||
|
new TimerTriggerMatcher()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 初始化匹配器管理器
|
||||||
|
matcherManager = new IotSceneRuleTriggerMatcherManager(matchers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDeviceStateUpdateTriggerMatcher() {
|
||||||
|
// 1. 准备测试数据
|
||||||
|
IotDeviceMessage message = IotDeviceMessage.builder()
|
||||||
|
.requestId("test-001")
|
||||||
|
.method("thing.state.update")
|
||||||
|
.data(1) // 在线状态
|
||||||
|
.build();
|
||||||
|
|
||||||
|
IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
|
||||||
|
trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE.getType());
|
||||||
|
trigger.setOperator("=");
|
||||||
|
trigger.setValue("1");
|
||||||
|
|
||||||
|
// 2. 执行测试
|
||||||
|
boolean matched = matcherManager.isMatched(message, trigger);
|
||||||
|
|
||||||
|
// 3. 验证结果
|
||||||
|
assertTrue(matched, "设备状态更新触发器应该匹配");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDevicePropertyPostTriggerMatcher() {
|
||||||
|
// 1. 准备测试数据
|
||||||
|
HashMap<String, Object> params = new HashMap<>();
|
||||||
|
IotDeviceMessage message = IotDeviceMessage.builder()
|
||||||
|
.requestId("test-002")
|
||||||
|
.method("thing.property.post")
|
||||||
|
.data(25.5) // 温度值
|
||||||
|
.params(params)
|
||||||
|
.build();
|
||||||
|
// 模拟标识符
|
||||||
|
params.put("identifier", "temperature");
|
||||||
|
|
||||||
|
IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
|
||||||
|
trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType());
|
||||||
|
trigger.setIdentifier("temperature");
|
||||||
|
trigger.setOperator(">");
|
||||||
|
trigger.setValue("20");
|
||||||
|
|
||||||
|
// 2. 执行测试
|
||||||
|
boolean matched = matcherManager.isMatched(message, trigger);
|
||||||
|
|
||||||
|
// 3. 验证结果
|
||||||
|
assertTrue(matched, "设备属性上报触发器应该匹配");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDeviceEventPostTriggerMatcher() {
|
||||||
|
// 1. 准备测试数据
|
||||||
|
HashMap<String, Object> params = new HashMap<>();
|
||||||
|
IotDeviceMessage message = IotDeviceMessage.builder()
|
||||||
|
.requestId("test-003")
|
||||||
|
.method("thing.event.post")
|
||||||
|
.data("alarm_data")
|
||||||
|
.params(params)
|
||||||
|
.build();
|
||||||
|
// 模拟标识符
|
||||||
|
params.put("identifier", "high_temperature_alarm");
|
||||||
|
|
||||||
|
IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
|
||||||
|
trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST.getType());
|
||||||
|
trigger.setIdentifier("high_temperature_alarm");
|
||||||
|
|
||||||
|
// 2. 执行测试
|
||||||
|
boolean matched = matcherManager.isMatched(message, trigger);
|
||||||
|
|
||||||
|
// 3. 验证结果
|
||||||
|
assertTrue(matched, "设备事件上报触发器应该匹配");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDeviceServiceInvokeTriggerMatcher() {
|
||||||
|
// 1. 准备测试数据
|
||||||
|
HashMap<String, Object> params = new HashMap<>();
|
||||||
|
IotDeviceMessage message = IotDeviceMessage.builder()
|
||||||
|
.requestId("test-004")
|
||||||
|
.method("thing.service.invoke")
|
||||||
|
.msg("alarm_data")
|
||||||
|
.params(params)
|
||||||
|
.build();
|
||||||
|
// 模拟标识符
|
||||||
|
params.put("identifier", "restart_device");
|
||||||
|
|
||||||
|
IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
|
||||||
|
trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
|
||||||
|
trigger.setIdentifier("restart_device");
|
||||||
|
|
||||||
|
// 2. 执行测试
|
||||||
|
boolean matched = matcherManager.isMatched(message, trigger);
|
||||||
|
|
||||||
|
// 3. 验证结果
|
||||||
|
assertTrue(matched, "设备服务调用触发器应该匹配");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testTimerTriggerMatcher() {
|
||||||
|
// 1. 准备测试数据
|
||||||
|
IotDeviceMessage message = IotDeviceMessage.builder()
|
||||||
|
.requestId("test-005")
|
||||||
|
.method("timer.trigger") // 定时触发器不依赖具体消息方法
|
||||||
|
.build();
|
||||||
|
|
||||||
|
IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
|
||||||
|
trigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
|
||||||
|
trigger.setCronExpression("0 0 12 * * ?"); // 每天中午12点
|
||||||
|
|
||||||
|
// 2. 执行测试
|
||||||
|
boolean matched = matcherManager.isMatched(message, trigger);
|
||||||
|
|
||||||
|
// 3. 验证结果
|
||||||
|
assertTrue(matched, "定时触发器应该匹配");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalidTriggerType() {
|
||||||
|
// 1. 准备测试数据
|
||||||
|
IotDeviceMessage message = IotDeviceMessage.builder()
|
||||||
|
.requestId("test-006")
|
||||||
|
.method("unknown.method")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
|
||||||
|
trigger.setType(999); // 无效的触发器类型
|
||||||
|
|
||||||
|
// 2. 执行测试
|
||||||
|
boolean matched = matcherManager.isMatched(message, trigger);
|
||||||
|
|
||||||
|
// 3. 验证结果
|
||||||
|
assertFalse(matched, "无效的触发器类型应该不匹配");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMatcherManagerStatistics() {
|
||||||
|
// 1. 执行测试
|
||||||
|
var statistics = matcherManager.getMatcherStatistics();
|
||||||
|
|
||||||
|
// 2. 验证结果
|
||||||
|
assertNotNull(statistics);
|
||||||
|
assertEquals(5, statistics.get("totalMatchers"));
|
||||||
|
assertEquals(5, statistics.get("enabledMatchers"));
|
||||||
|
assertNotNull(statistics.get("supportedTriggerTypes"));
|
||||||
|
assertNotNull(statistics.get("matcherDetails"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetSupportedTriggerTypes() {
|
||||||
|
// 1. 执行测试
|
||||||
|
var supportedTypes = matcherManager.getSupportedTriggerTypes();
|
||||||
|
|
||||||
|
// 2. 验证结果
|
||||||
|
assertNotNull(supportedTypes);
|
||||||
|
assertEquals(5, supportedTypes.size());
|
||||||
|
assertTrue(supportedTypes.contains(IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE));
|
||||||
|
assertTrue(supportedTypes.contains(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST));
|
||||||
|
assertTrue(supportedTypes.contains(IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST));
|
||||||
|
assertTrue(supportedTypes.contains(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE));
|
||||||
|
assertTrue(supportedTypes.contains(IotSceneRuleTriggerTypeEnum.TIMER));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user