|
|
|
|
@ -1,17 +1,35 @@
|
|
|
|
|
package cn.iocoder.yudao.module.iot.job;
|
|
|
|
|
|
|
|
|
|
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
|
|
|
|
|
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
|
|
|
|
import cn.iocoder.yudao.module.iot.controller.admin.device.enums.DeviceStatusEnum;
|
|
|
|
|
import cn.iocoder.yudao.module.iot.controller.admin.devicemodelrules.vo.PointRulesRespVO;
|
|
|
|
|
import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO;
|
|
|
|
|
import cn.iocoder.yudao.module.iot.dal.dataobject.devicecontactmodel.DeviceContactModelDO;
|
|
|
|
|
import cn.iocoder.yudao.module.iot.dal.dataobject.deviceoperationrecord.DeviceOperationRecordDO;
|
|
|
|
|
import cn.iocoder.yudao.module.iot.dal.dataobject.devicepointrules.DevicePointRulesDO;
|
|
|
|
|
import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceMapper;
|
|
|
|
|
import cn.iocoder.yudao.module.iot.dal.mysql.devicecontactmodel.DeviceContactModelMapper;
|
|
|
|
|
import cn.iocoder.yudao.module.iot.dal.mysql.deviceoperationrecord.DeviceOperationRecordMapper;
|
|
|
|
|
import cn.iocoder.yudao.module.iot.dal.mysql.devicepointrules.DevicePointRulesMapper;
|
|
|
|
|
import cn.iocoder.yudao.module.iot.service.device.TDengineService;
|
|
|
|
|
import com.alibaba.fastjson.JSON;
|
|
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
|
|
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
|
|
|
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
|
|
|
|
|
|
|
import java.util.Date;
|
|
|
|
|
import java.sql.Timestamp;
|
|
|
|
|
import java.time.Duration;
|
|
|
|
|
import java.time.Instant;
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
import java.time.ZoneId;
|
|
|
|
|
import java.util.*;
|
|
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
|
|
|
|
|
|
|
|
|
@ -28,35 +46,354 @@ public class DeviceJob implements JobHandler {
|
|
|
|
|
@Resource
|
|
|
|
|
private DeviceContactModelMapper deviceContactModelMapper;
|
|
|
|
|
|
|
|
|
|
@Resource
|
|
|
|
|
private DeviceOperationRecordMapper deviceOperationRecordMapper;
|
|
|
|
|
|
|
|
|
|
@Resource
|
|
|
|
|
private DevicePointRulesMapper devicePointRulesMapper;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public String execute(String param) throws Exception {
|
|
|
|
|
// 设置租户上下文
|
|
|
|
|
TenantContextHolder.setTenantId(1L);
|
|
|
|
|
|
|
|
|
|
// 解析超时时间(默认60秒)
|
|
|
|
|
long timeoutSeconds = 60L;
|
|
|
|
|
if (StringUtils.isNotBlank(param)) {
|
|
|
|
|
try {
|
|
|
|
|
timeoutSeconds = Long.parseLong(param);
|
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
|
log.warn("定时任务参数非法,使用默认60秒 param={}", param);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.info("定时任务开始, timeoutSeconds={} 时间={}", timeoutSeconds, new Date());
|
|
|
|
|
|
|
|
|
|
// 查询采集设备列表
|
|
|
|
|
List<DeviceDO> deviceDOS =
|
|
|
|
|
deviceMapper.selectList(Wrappers.<DeviceDO>lambdaQuery().in(DeviceDO::getId, Arrays.asList(142L, 140L) ).orderByDesc(DeviceDO::getId));
|
|
|
|
|
|
|
|
|
|
if (CollectionUtils.isEmpty(deviceDOS)) {
|
|
|
|
|
return param;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<Long> deviceIds = deviceDOS.stream()
|
|
|
|
|
.map(DeviceDO::getId)
|
|
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
|
|
|
|
// 获取设备的每条最新数据
|
|
|
|
|
Map<Long, Map<String, Object>> deviceRowMap =
|
|
|
|
|
tDengineService.queryDevicesLatestRow(deviceIds, null, null);
|
|
|
|
|
|
|
|
|
|
Instant now = Instant.now();
|
|
|
|
|
|
|
|
|
|
// 遍历设备
|
|
|
|
|
for (Long deviceId : deviceIds) {
|
|
|
|
|
Map<String, Object> row = deviceRowMap.get(deviceId);
|
|
|
|
|
boolean isTimeout = false;
|
|
|
|
|
|
|
|
|
|
if (row == null || row.get("ts") == null) {
|
|
|
|
|
isTimeout = true;
|
|
|
|
|
} else {
|
|
|
|
|
Instant ts = parseTs(row.get("ts"), deviceId);
|
|
|
|
|
if (ts == null || Duration.between(ts, now).getSeconds() > timeoutSeconds) {
|
|
|
|
|
isTimeout = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isTimeout) {
|
|
|
|
|
handleDeviceTimeout(deviceId);
|
|
|
|
|
} else {
|
|
|
|
|
handleDeviceOnline(deviceId, row);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return param;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析JSON字符串获取deviceId
|
|
|
|
|
JSONObject jsonParam = JSON.parseObject(param);
|
|
|
|
|
System.out.println(jsonParam + new Date().toString());
|
|
|
|
|
// Long deviceId = jsonParam.getLong("deviceId");
|
|
|
|
|
// log.info("定时任务执行,接收到的参数 param: {}", param);
|
|
|
|
|
// if (deviceId == null){
|
|
|
|
|
// throw exception(DEVICE_DOES_NOT_EXIST);
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// // 设置租户上下文
|
|
|
|
|
// TenantContextHolder.setTenantId(1L);
|
|
|
|
|
//
|
|
|
|
|
// LambdaQueryWrapper<DeviceContactModelDO> deviceModelAttributeLambdaQueryWrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
// deviceModelAttributeLambdaQueryWrapper.eq(DeviceContactModelDO::getDeviceId,deviceId);
|
|
|
|
|
// List<DeviceContactModelDO> deviceContactModelDOS = deviceContactModelMapper.selectList(deviceModelAttributeLambdaQueryWrapper);
|
|
|
|
|
//
|
|
|
|
|
// if (deviceContactModelDOS != null && deviceContactModelDOS.size() > 0){
|
|
|
|
|
// for (DeviceContactModelDO deviceContactModelDO : deviceContactModelDOS) {
|
|
|
|
|
// Object addressValue = OpcUtils.readValue(deviceContactModelDO.getAddress() != null ? deviceContactModelDO.getAddress() : "");
|
|
|
|
|
// deviceContactModelDO.setAddressValue(addressValue);
|
|
|
|
|
// }
|
|
|
|
|
//
|
|
|
|
|
// }
|
|
|
|
|
// String json = JSON.toJSONString(deviceContactModelDOS);
|
|
|
|
|
// tDengineService.insertDeviceData(deviceId,json);
|
|
|
|
|
|
|
|
|
|
return "";
|
|
|
|
|
/**
|
|
|
|
|
* 安全转换 ts 为 Instant
|
|
|
|
|
*/
|
|
|
|
|
private Instant parseTs(Object tsObj, Long deviceId) {
|
|
|
|
|
if (tsObj == null) return null;
|
|
|
|
|
|
|
|
|
|
log.debug("设备 {} tsObj 类型: {}, 值: {}", deviceId, tsObj.getClass().getName(), tsObj);
|
|
|
|
|
|
|
|
|
|
if (tsObj instanceof Instant) {
|
|
|
|
|
return (Instant) tsObj;
|
|
|
|
|
} else if (tsObj instanceof Timestamp) {
|
|
|
|
|
return ((Timestamp) tsObj).toInstant();
|
|
|
|
|
} else if (tsObj instanceof Date) {
|
|
|
|
|
return ((Date) tsObj).toInstant();
|
|
|
|
|
} else if (tsObj instanceof LocalDateTime) {
|
|
|
|
|
return ((LocalDateTime) tsObj).atZone(ZoneId.systemDefault()).toInstant();
|
|
|
|
|
} else if (tsObj instanceof String) {
|
|
|
|
|
String tsStr = (String) tsObj;
|
|
|
|
|
try {
|
|
|
|
|
return Instant.parse(tsStr); // ISO 8601
|
|
|
|
|
} catch (Exception e1) {
|
|
|
|
|
try {
|
|
|
|
|
return Timestamp.valueOf(tsStr).toInstant(); // yyyy-MM-dd HH:mm:ss
|
|
|
|
|
} catch (Exception e2) {
|
|
|
|
|
log.warn("设备 {} ts 字符串解析失败: {}", deviceId, tsStr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
log.warn("设备 {} ts 类型未知: {}", deviceId, tsObj);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设备在线处理
|
|
|
|
|
*/
|
|
|
|
|
private void handleDeviceOnline(Long deviceId, Map<String, Object> row) {
|
|
|
|
|
if (row == null) return;
|
|
|
|
|
|
|
|
|
|
// 1. 查询设备规则
|
|
|
|
|
DevicePointRulesDO pointRulesDO = devicePointRulesMapper.selectOne(
|
|
|
|
|
Wrappers.<DevicePointRulesDO>lambdaQuery()
|
|
|
|
|
.eq(DevicePointRulesDO::getDeviceId, deviceId)
|
|
|
|
|
.eq(DevicePointRulesDO::getIdentifier, "RUNNING")
|
|
|
|
|
.orderByDesc(DevicePointRulesDO::getId)
|
|
|
|
|
.last("LIMIT 1")
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (pointRulesDO == null || StringUtils.isBlank(pointRulesDO.getFieldRule())) return;
|
|
|
|
|
|
|
|
|
|
// 解析规则列表
|
|
|
|
|
List<PointRulesRespVO> pointRulesVOList = JSON.parseArray(
|
|
|
|
|
pointRulesDO.getFieldRule(), PointRulesRespVO.class
|
|
|
|
|
);
|
|
|
|
|
if (CollectionUtils.isEmpty(pointRulesVOList)) return;
|
|
|
|
|
|
|
|
|
|
// 2. 查询设备 contact model
|
|
|
|
|
List<DeviceContactModelDO> deviceContactModelDOS = deviceContactModelMapper.selectList(
|
|
|
|
|
Wrappers.<DeviceContactModelDO>lambdaQuery().eq(DeviceContactModelDO::getDeviceId, deviceId)
|
|
|
|
|
);
|
|
|
|
|
if (CollectionUtils.isEmpty(deviceContactModelDOS)) return;
|
|
|
|
|
|
|
|
|
|
// 3. 遍历规则,匹配成功则保存记录
|
|
|
|
|
for (PointRulesRespVO pointRule : pointRulesVOList) {
|
|
|
|
|
if (StringUtils.isBlank(pointRule.getCode())) continue;
|
|
|
|
|
|
|
|
|
|
String ruleCode = pointRule.getCode().toLowerCase();
|
|
|
|
|
String processedValue = row.get(ruleCode).toString();
|
|
|
|
|
boolean matched = matchRule(processedValue, pointRule);
|
|
|
|
|
|
|
|
|
|
if (!matched) {
|
|
|
|
|
log.debug("规则匹配失败: device={}, value={}, rule={}", deviceId, processedValue, JSON.toJSONString(pointRule));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.info("规则匹配成功: device={}, value={}, rule={}", deviceId, processedValue, JSON.toJSONString(pointRule));
|
|
|
|
|
|
|
|
|
|
// 4. 遍历 contact model 查找对应 code
|
|
|
|
|
DeviceContactModelDO matchedContact = null;
|
|
|
|
|
for (DeviceContactModelDO contact : deviceContactModelDOS) {
|
|
|
|
|
if (ruleCode.equalsIgnoreCase(contact.getAttributeCode())) {
|
|
|
|
|
matchedContact = contact;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (matchedContact == null) {
|
|
|
|
|
log.warn("设备 {} 找不到 attributeCode={} 对应的 modelId,跳过", deviceId, pointRule.getCode());
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. 保存运行记录
|
|
|
|
|
DeviceOperationRecordDO record = new DeviceOperationRecordDO();
|
|
|
|
|
record.setDeviceId(deviceId);
|
|
|
|
|
record.setModelId(matchedContact.getId());
|
|
|
|
|
record.setRule(pointRule.getRule());
|
|
|
|
|
record.setAddressValue(processedValue);
|
|
|
|
|
record.setRuleId(pointRulesDO.getId());
|
|
|
|
|
record.setCreator("1");
|
|
|
|
|
record.setUpdater("1");
|
|
|
|
|
|
|
|
|
|
deviceOperationRecordMapper.insert(record);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void handleDeviceTimeout(Long deviceId) {
|
|
|
|
|
DeviceOperationRecordDO record = new DeviceOperationRecordDO();
|
|
|
|
|
record.setDeviceId(deviceId);
|
|
|
|
|
record.setRule(DeviceStatusEnum.OFFLINE.getCode());
|
|
|
|
|
record.setCreator("1");
|
|
|
|
|
record.setUpdater("1");
|
|
|
|
|
deviceOperationRecordMapper.insert(record);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 判断值是否符合规则
|
|
|
|
|
* 支持操作符: EQ(等于), NE(不等于), GT(大于), GE(大于等于),
|
|
|
|
|
* LT(小于), LE(小于等于), TRUE(为真), FALSE(为假)
|
|
|
|
|
*/
|
|
|
|
|
private boolean matchRule(String value, PointRulesRespVO rule) {
|
|
|
|
|
if (StringUtils.isBlank(value) || rule == null ||
|
|
|
|
|
StringUtils.isBlank(rule.getOperator())) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
String operator = rule.getOperator().toUpperCase();
|
|
|
|
|
String inputValue = value.trim().toLowerCase();
|
|
|
|
|
String ruleValue = StringUtils.trimToEmpty(rule.getOperatorRule());
|
|
|
|
|
|
|
|
|
|
// 1. 处理布尔值判断
|
|
|
|
|
if ("TRUE".equals(operator) || "FALSE".equals(operator)) {
|
|
|
|
|
return matchBooleanRule(inputValue, operator);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 如果operatorRule为空,且不是布尔操作符,则返回false
|
|
|
|
|
if (StringUtils.isBlank(ruleValue)) {
|
|
|
|
|
log.warn("规则比较值为空,但操作符不是布尔类型: operator={}", operator);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ruleValue = ruleValue.trim();
|
|
|
|
|
|
|
|
|
|
// 3. 尝试数值比较
|
|
|
|
|
if (isNumeric(inputValue) && isNumeric(ruleValue)) {
|
|
|
|
|
Double num1 = Double.parseDouble(inputValue);
|
|
|
|
|
Double num2 = Double.parseDouble(ruleValue);
|
|
|
|
|
|
|
|
|
|
return compareNumbers(num1, num2, operator);
|
|
|
|
|
}
|
|
|
|
|
// 4. 字符串比较
|
|
|
|
|
else {
|
|
|
|
|
return compareStrings(inputValue, ruleValue, operator);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.error("规则匹配异常: value={}, rule={}, error={}",
|
|
|
|
|
value, JSON.toJSONString(rule), e.getMessage());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 字符串比较
|
|
|
|
|
*/
|
|
|
|
|
private boolean compareStrings(String value, String ruleValue, String operator) {
|
|
|
|
|
switch (operator) {
|
|
|
|
|
case "EQ":
|
|
|
|
|
return value.equals(ruleValue);
|
|
|
|
|
case "NE":
|
|
|
|
|
return !value.equals(ruleValue);
|
|
|
|
|
case "GT":
|
|
|
|
|
return value.compareTo(ruleValue) > 0;
|
|
|
|
|
case "GE":
|
|
|
|
|
return value.compareTo(ruleValue) >= 0;
|
|
|
|
|
case "LT":
|
|
|
|
|
return value.compareTo(ruleValue) < 0;
|
|
|
|
|
case "LE":
|
|
|
|
|
return value.compareTo(ruleValue) <= 0;
|
|
|
|
|
default:
|
|
|
|
|
log.warn("不支持的操作符: {}", operator);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 数值比较
|
|
|
|
|
*/
|
|
|
|
|
private boolean compareNumbers(Double value, Double ruleValue, String operator) {
|
|
|
|
|
switch (operator) {
|
|
|
|
|
case "EQ":
|
|
|
|
|
return Math.abs(value - ruleValue) < 0.000001; // 处理浮点数精度
|
|
|
|
|
case "NE":
|
|
|
|
|
return Math.abs(value - ruleValue) >= 0.000001;
|
|
|
|
|
case "GT":
|
|
|
|
|
return value > ruleValue;
|
|
|
|
|
case "GE":
|
|
|
|
|
return value >= ruleValue;
|
|
|
|
|
case "LT":
|
|
|
|
|
return value < ruleValue;
|
|
|
|
|
case "LE":
|
|
|
|
|
return value <= ruleValue;
|
|
|
|
|
default:
|
|
|
|
|
log.warn("不支持的操作符: {}", operator);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 判断字符串是否为数字
|
|
|
|
|
*/
|
|
|
|
|
private boolean isNumeric(String str) {
|
|
|
|
|
if (StringUtils.isBlank(str)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
Double.parseDouble(str);
|
|
|
|
|
return true;
|
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理布尔值判断
|
|
|
|
|
*/
|
|
|
|
|
private boolean matchBooleanRule(String value, String operator) {
|
|
|
|
|
// 常见布尔值表示
|
|
|
|
|
boolean booleanValue = parseBoolean(value);
|
|
|
|
|
|
|
|
|
|
if ("TRUE".equals(operator)) {
|
|
|
|
|
return booleanValue;
|
|
|
|
|
} else if ("FALSE".equals(operator)) {
|
|
|
|
|
return !booleanValue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 解析字符串为布尔值
|
|
|
|
|
* 支持: true, false, 1, 0, yes, no, on, off等
|
|
|
|
|
*/
|
|
|
|
|
private boolean parseBoolean(String value) {
|
|
|
|
|
if (StringUtils.isBlank(value)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String lowerValue = value.toLowerCase();
|
|
|
|
|
|
|
|
|
|
// 常见真值表示
|
|
|
|
|
if ("true".equals(lowerValue) ||
|
|
|
|
|
"1".equals(lowerValue) ||
|
|
|
|
|
"yes".equals(lowerValue) ||
|
|
|
|
|
"on".equals(lowerValue) ||
|
|
|
|
|
"是".equals(lowerValue) || // 中文支持
|
|
|
|
|
"成功".equals(lowerValue)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 常见假值表示
|
|
|
|
|
if ("false".equals(lowerValue) ||
|
|
|
|
|
"0".equals(lowerValue) ||
|
|
|
|
|
"no".equals(lowerValue) ||
|
|
|
|
|
"off".equals(lowerValue) ||
|
|
|
|
|
"否".equals(lowerValue) || // 中文支持
|
|
|
|
|
"失败".equals(lowerValue)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 尝试转换为布尔值
|
|
|
|
|
try {
|
|
|
|
|
return Boolean.parseBoolean(lowerValue);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.warn("无法解析为布尔值: {}", value);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|