Browse Source

feate: 设备断电预警

jackzhou 4 months ago
parent
commit
4ec31ab8f5
13 changed files with 246 additions and 8 deletions
  1. 3 0
      snowy-common/src/main/java/vip/xiaonuo/common/cache/CommonCacheOperator.java
  2. 7 1
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/bean/SensorAlarm.java
  3. 3 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/config/ColdChainAlarmMessageProperties.java
  4. 195 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/offline/DeviceOfflineDetectionService.java
  5. 1 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/service/SensorAlarmServiceImpl.java
  6. 7 1
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/service/check/DefaultSensorAlarmChecker.java
  7. 8 1
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/service/messagepush/impl/WechatMessagePushService.java
  8. 1 1
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/cache/monitordevice/MonitorDeviceCacheInitializer.java
  9. 2 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/cache/monitordevice/impl/RedisMonitorDeviceCacheService.java
  10. 1 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/config/ColdChainAsyncConfig.java
  11. 10 4
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/config/JfcloudColdChainConstants.java
  12. 5 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/renke/config/JfcloudColdChainServerProperties.java
  13. 3 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/push/param/PushParam.java

+ 3 - 0
snowy-common/src/main/java/vip/xiaonuo/common/cache/CommonCacheOperator.java

@@ -45,6 +45,9 @@ public class CommonCacheOperator {
     public void put(String key, Object value, long timeoutSeconds) {
         redisTemplate.boundValueOps(CACHE_KEY_PREFIX + key).set(value, timeoutSeconds, TimeUnit.SECONDS);
     }
+    public void put(String key, Object value, long timeout, TimeUnit unit) {
+        redisTemplate.boundValueOps(CACHE_KEY_PREFIX + key).set(value, timeout, unit);
+    }
 
     public Object get(String key) {
         return redisTemplate.boundValueOps(CACHE_KEY_PREFIX + key).get();

+ 7 - 1
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/bean/SensorAlarm.java

@@ -46,6 +46,12 @@ public class SensorAlarm extends OrgEntity {
     @Schema(description = "告警接收人,存储告警通知的接收用户信息")
     private List<SensorAlarmUser> alarmUsers = Lists.newArrayList();
 
+    /**
+     * 预警类型(0:数据异常 1:设备离线)
+     */
+    @Schema(description = "类型")
+    private String type;
+
     /**
      * 告警类型,例如温度过高、湿度过低等
      */
@@ -123,7 +129,7 @@ public class SensorAlarm extends OrgEntity {
      * 告警触发阈值,记录触发告警的具体阈值
      */
     @Schema(description = "告警触发阈值,记录触发告警的具体阈值")
-    private float threshold;
+    private Float threshold;
 
 
     @Schema(description = "微信请求状态码")

+ 3 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/config/ColdChainAlarmMessageProperties.java

@@ -31,4 +31,7 @@ public class ColdChainAlarmMessageProperties {
     private String co2BelowLimit = "二氧化碳报警:{deviceName}的二氧化碳浓度过低!\n" +
             "当前浓度:{value} {unit},已低于下限(阈值:{threshold})。\n" +
             "报警时间:{time}";
+
+    private String deviceOfflineLimit = "设备离线:{deviceName}的设备断电或者离线\n" +
+            "报警时间:{time}";
 }

+ 195 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/offline/DeviceOfflineDetectionService.java

@@ -0,0 +1,195 @@
+package vip.xiaonuo.coldchain.core.alarm.offline;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import vip.xiaonuo.coldchain.core.alarm.bean.SensorAlarm;
+import vip.xiaonuo.coldchain.core.alarm.bean.SensorAlarmUser;
+import vip.xiaonuo.coldchain.core.alarm.config.ColdChainAlarmMessageProperties;
+import vip.xiaonuo.coldchain.core.config.JfcloudColdChainConstants;
+import vip.xiaonuo.coldchain.core.event.SensorAlarmEvent;
+import vip.xiaonuo.coldchain.core.renke.config.JfcloudColdChainServerProperties;
+import vip.xiaonuo.coldchain.modular.monitortargetregion.entity.MonitorTargetRegion;
+import vip.xiaonuo.coldchain.modular.monitortargetregion.service.MonitorTargetRegionService;
+import vip.xiaonuo.common.cache.CommonCacheOperator;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author jackzhou
+ * @version 1.0
+ * @project jfcloud-coldchain
+ * @description
+ * @date 2025/1/5 10:50:04
+ */
+
+@RequiredArgsConstructor
+@Service
+@Slf4j
+public class DeviceOfflineDetectionService {
+    // 时间格式化器,用于格式化时间为指定格式
+    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    private static final String CACHE_KEY_PREFIX = "Cache:DeviceOffline:";
+    private final CommonCacheOperator commonCacheOperator;
+    private final ApplicationEventPublisher applicationEventPublisher;
+    private final MonitorTargetRegionService monitorTargetRegionService;
+    private final ColdChainAlarmMessageProperties alarmMessageProperties;
+    private final JfcloudColdChainServerProperties jfcloudColdChainServerProperties;
+    private static final String KEY_SPILT = "-";
+
+
+    /**
+     * 异步上报设备数据
+     *
+     * @param deviceCode 设备标识符
+     */
+    @Async("coldChainAsyncTask")
+    public void reportDeviceTime(String deviceCode, Integer route) {
+        // 获取当前时间并格式化为字符串
+        String currentTime = LocalDateTime.now().format(DATE_TIME_FORMATTER);
+        // 将设备的上报时间存储到 Redis 中,并设置过期时间(离线阈值时间+1分钟)
+        cacheDeviceTime(deviceCode, route, currentTime);
+        log.info("设备 {} 异步上报数据,时间:{}\n", getKey(deviceCode, route), currentTime);
+    }
+
+    /**
+     * 定期检测设备状态是否离线
+     * 每 5 分钟运行一次
+     */
+    @Scheduled(fixedRate = JfcloudColdChainConstants.OFFLINE_THRESHOLD_SECONDS) // 每 5 分钟(以毫秒为单位)
+    public void checkDeviceStatus() {
+        log.info("开始检测离线设备状态...");
+        // 获取 Redis 中所有设备的键
+        Set<String> deviceKeys = getAllDeviceCodes();
+        if (deviceKeys == null || deviceKeys.isEmpty()) {
+            log.info("没有需要检测的设备。");
+            return;
+        }
+        // 遍历设备键,逐一检测状态
+        for (String key : deviceKeys) {
+            String[] split = key.split(KEY_SPILT);
+            if (split.length == 0) {
+                continue;
+            }
+            String deviceCode = split[0];
+            Integer route = null;
+            if (split.length == 2) {
+                route = Integer.valueOf(split[1]);
+            }
+            // 从 Redis 中获取设备的最后上报时间
+            String lastReportTimeStr = getLastDeviceTime(getKey(deviceCode,route));
+            if (lastReportTimeStr != null) {
+                // 将上报时间字符串解析为 LocalDateTime 对象
+                LocalDateTime lastReportTime = LocalDateTime.parse(lastReportTimeStr, DATE_TIME_FORMATTER);
+                // 获取当前时间
+                LocalDateTime now = LocalDateTime.now();
+                final long OFFLINE_THRESHOLD_MINUTES = jfcloudColdChainServerProperties.getDeviceOfflineInterval() / 60 / 1000;
+                // 如果最后上报时间超过离线阈值,判断设备为离线
+                if (lastReportTime.plusMinutes(OFFLINE_THRESHOLD_MINUTES).isBefore(now)) {
+                    log.error("设备{}-{} 已离线,最后上报时间:{}。\n", deviceCode, route, lastReportTimeStr);
+                    publishAlarm(deviceCode, route, lastReportTimeStr);
+                    // TODO 标识设备离线
+                } else {
+                    // TODO 标识设备在线
+                    log.info("设备 {}-{} 在线,最后上报时间:{}。\n", deviceCode, route, lastReportTimeStr);
+                }
+            } else {
+                // 如果设备上报时间为空,认为设备可能从未上报过数据
+                log.info("设备 {}-{} 缺少数据,可能从未上报过。\n", deviceCode, route);
+            }
+        }
+    }
+
+
+    private void publishAlarm(String deviceCode, Integer route, String time) {
+        MonitorTargetRegion monitorTargetRegion = monitorTargetRegionService.findOneByDeviceCodeAndSensorNo(deviceCode, route);
+        if (Objects.isNull(monitorTargetRegion)) {
+            return;
+        }
+        String monitorTargetId = monitorTargetRegion.getMonitorTargetId();
+        Integer sensorRoute = monitorTargetRegion.getSensorRoute();
+        String sensorCode = monitorTargetRegion.getSensorCode();
+        String deviceName = monitorTargetRegion.getName();
+        String deviceId = monitorTargetRegion.getId();
+        final String alarmType = "设备离线";
+        String alarmMessage = getAlarmMessage(deviceName, time);
+        SensorAlarm sensorAlarm = new SensorAlarm();
+        sensorAlarm.setAlarmType(alarmType);
+        sensorAlarm.setValue(0f);
+        sensorAlarm.setAlarmTime(time);
+        sensorAlarm.setSource(deviceName);  // 设置设备名称
+        sensorAlarm.setDeviceName(deviceName);  // 设置设备名称
+        sensorAlarm.setDeviceId(deviceId);  // 设置设备名称
+        sensorAlarm.setPriority("高");  // 设置设备名称
+        sensorAlarm.setStatus("未处理");  // 设置设备名称
+        sensorAlarm.setAlarmTime(time);  // 设置报警时间
+        sensorAlarm.setMessage(alarmMessage);  // 设置报警消息
+        sensorAlarm.setThreshold(null);  // 设置预警值
+        sensorAlarm.setMonitorTargetId(monitorTargetId);
+        sensorAlarm.setSensorRoute(sensorRoute);
+        sensorAlarm.setSensorCode(sensorCode);
+        sensorAlarm.setCreateUser(monitorTargetRegion.getCreateUser());
+        List<SensorAlarmUser> alarmUsers = monitorTargetRegion.getAlarmUsers();
+        sensorAlarm.setAlarmUsers(alarmUsers);
+        sensorAlarm.setThreshold(0f);
+        // 设置报警人机构 所属用户和机构
+        sensorAlarm.setCreateUser(monitorTargetRegion.getCreateUser());
+        sensorAlarm.setType("1");
+        log.warn("设备断电报警: 类型: {},详细报警内容 : {}", alarmType, alarmMessage);
+        // 发布报警事件
+        applicationEventPublisher.publishEvent(new SensorAlarmEvent(this, sensorAlarm));
+    }
+
+    private String getAlarmMessage(String deviceName, String time) {
+        String messageTemplate = alarmMessageProperties.getDeviceOfflineLimit();
+        return messageTemplate.replace("{deviceName}", deviceName).replace("{time}", time);
+    }
+
+    /**
+     * 缓存设备时间
+     *
+     * @param deviceCode
+     * @param route
+     * @param time
+     */
+    private void cacheDeviceTime(String deviceCode, Integer route, String time) {
+        final long OFFLINE_THRESHOLD_MINUTES = jfcloudColdChainServerProperties.getDeviceOfflineInterval() / 60 / 1000;
+        commonCacheOperator.put(getKey(deviceCode, route), time/*, OFFLINE_THRESHOLD_MINUTES + 1, TimeUnit.MINUTES*/);
+    }
+
+    @NotNull
+    private static String getKey(String deviceCode, Integer route) {
+        if (Objects.nonNull(route)) {
+            return CACHE_KEY_PREFIX + deviceCode + KEY_SPILT + route;
+        }
+        return CACHE_KEY_PREFIX + deviceCode;
+    }
+
+    private String getLastDeviceTime(String key) {
+        if (exists(key)) {
+            return (String) commonCacheOperator.get(key);
+        } else {
+            return null;
+        }
+    }
+
+    private boolean exists(String key) {
+        return commonCacheOperator.get(key) != null;
+    }
+
+    private Set<String> getAllDeviceCodes() {
+        return commonCacheOperator.getAllKeys().stream()
+                .filter(key -> key.startsWith(CACHE_KEY_PREFIX)) // 过滤指定前缀的键
+                .map(key -> key.replace(CACHE_KEY_PREFIX, "")) // 去掉前缀
+                .collect(Collectors.toSet());
+    }
+}

+ 1 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/service/SensorAlarmServiceImpl.java

@@ -98,6 +98,7 @@ public class SensorAlarmServiceImpl extends ServiceImpl<SensorAlarmMapper, Senso
         String orgId = StpLoginUserUtil.getLoginUser().getOrgId();
         // 查询创建用户为当前用户的记录
         queryWrapper.lambda().eq(SensorAlarm::getCreateOrg, orgId);
+        queryWrapper.lambda().eq(SensorAlarm::getType, "0");
         // 如果有关键词,进行模糊查询或者设备名等字段的精确查询
         if (ObjectUtil.isNotEmpty(messagePageParam.getKeyword())) {
             queryWrapper.lambda().and(q -> q.like(SensorAlarm::getMessage, messagePageParam.getKeyword()).or().like(SensorAlarm::getDeviceName, messagePageParam.getKeyword()));

+ 7 - 1
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/service/check/DefaultSensorAlarmChecker.java

@@ -9,6 +9,7 @@ import vip.xiaonuo.coldchain.core.alarm.bean.SensorAlarm;
 import vip.xiaonuo.coldchain.core.alarm.bean.SensorAlarmUser;
 import vip.xiaonuo.coldchain.core.alarm.bean.SensorThreshold;
 import vip.xiaonuo.coldchain.core.alarm.config.ColdChainAlarmMessageProperties;
+import vip.xiaonuo.coldchain.core.alarm.offline.DeviceOfflineDetectionService;
 import vip.xiaonuo.coldchain.core.alarm.service.threshold.SensorThresholdService;
 import vip.xiaonuo.coldchain.core.bean.influxdb.SensorData;
 import vip.xiaonuo.coldchain.core.event.SensorAlarmEvent;
@@ -28,11 +29,15 @@ public class DefaultSensorAlarmChecker implements SensorAlarmChecker {
     private final ColdChainAlarmMessageProperties alarmMessageProperties;
     private final ApplicationEventPublisher applicationEventPublisher;
 
+    private final DeviceOfflineDetectionService deviceOfflineDetectionService;
+
     @Override
     public boolean checkAlarm(SensorData sensorData) {
         if (sensorData == null) {
             throw new IllegalArgumentException("传感器数据不能为空!");
         }
+        //记录数据报送时间
+        deviceOfflineDetectionService.reportDeviceTime(sensorData.getDeviceId(), sensorData.getRoads());
         // 获取传感器阈值配置
         SensorThreshold threshold = thresholdService.getThresholdBySensorId(sensorData.getDeviceId(), sensorData.getRoads());
         if (threshold == null) {
@@ -105,7 +110,7 @@ public class DefaultSensorAlarmChecker implements SensorAlarmChecker {
         sensorAlarm.setSource(deviceName);  // 设置设备名称
         sensorAlarm.setDeviceName(deviceName);  // 设置设备名称
         sensorAlarm.setDeviceId(deviceId);  // 设置设备名称
-        sensorAlarm.setPriority("");  // 设置设备名称
+        sensorAlarm.setPriority("");  // 设置设备名称
         sensorAlarm.setStatus("未处理");  // 设置设备名称
         sensorAlarm.setAlarmTime(time);  // 设置报警时间
         sensorAlarm.setMessage(alarmMessage);  // 设置报警消息
@@ -117,6 +122,7 @@ public class DefaultSensorAlarmChecker implements SensorAlarmChecker {
         List<SensorAlarmUser> alarmUsers = monitorTargetRegion.getAlarmUsers();
         sensorAlarm.setAlarmUsers(alarmUsers);
         sensorAlarm.setThreshold(threshold);
+        sensorAlarm.setType("0");
         // 设置报警人机构 所属用户和机构
         sensorAlarm.setCreateUser(monitorTargetRegion.getCreateUser());
         sensorAlarm.setCreateOrg(monitorTargetRegion.getCreateOrg());

+ 8 - 1
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/alarm/service/messagepush/impl/WechatMessagePushService.java

@@ -21,7 +21,14 @@ public class WechatMessagePushService implements MessagePushService {
 
     @Override
     public boolean sendAlarmMessage(SensorAlarm alarm/*, SensorAlarmUser user*/) {
-        PushParam pushParam = new PushParam(alarm.getValue() + "/"+alarm.getThreshold());
+        PushParam pushParam = null;
+        if (alarm.getValue() == 0 || alarm.getThreshold() == 0) {
+            pushParam = new PushParam();
+            pushParam.setValue("设备离线/断电");
+        } else {
+            //数据异常
+            pushParam = new PushParam(alarm.getValue() + "/" + alarm.getThreshold());
+        }
         if (alarm.getMessage().length() > 17) {
             pushParam.setContext(alarm.getMessage().substring(0, 17) + "...");
         } else {

+ 1 - 1
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/cache/monitordevice/MonitorDeviceCacheInitializer.java

@@ -76,7 +76,7 @@ public class MonitorDeviceCacheInitializer implements CommandLineRunner {
     /**
      * 每 5 分钟定期执行设备缓存更新
      */
-    @Scheduled(fixedRateString = "${coldchain.cache-update-interval:300000}")
+    @Scheduled(fixedRateString = "${coldchain.cache-update-interval:60000}")
     public void updateDeviceCachePeriodically() {
         log.info("定期调用[deviceCode][model_name]缓存更新方法");
         updateDeviceCache();

+ 2 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/cache/monitordevice/impl/RedisMonitorDeviceCacheService.java

@@ -58,6 +58,7 @@ public class RedisMonitorDeviceCacheService implements MonitorDeviceCacheService
     @Override
     public Set<String> getAllDeviceCodes() {
         return commonCacheOperator.getAllKeys().stream()
+                .filter(key -> key.startsWith(CACHE_KEY_PREFIX))
                 .map(key -> key.replace(CACHE_KEY_PREFIX, ""))
                 .collect(Collectors.toSet());
     }
@@ -65,6 +66,7 @@ public class RedisMonitorDeviceCacheService implements MonitorDeviceCacheService
     @Override
     public List<String> getAllDeviceModels() {
         return commonCacheOperator.getAllKeys().stream()
+                .filter(key -> key.startsWith(CACHE_KEY_PREFIX))
                 .map(key -> (String) commonCacheOperator.get(key))
                 .collect(Collectors.toList());
     }

+ 1 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/config/ColdChainAsyncConfig.java

@@ -29,4 +29,5 @@ public class ColdChainAsyncConfig {
         executor.initialize();
         return executor;
     }
+
 }

+ 10 - 4
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/config/JfcloudColdChainConstants.java

@@ -22,7 +22,7 @@ public interface JfcloudColdChainConstants {
     /**
      * 每 5 分钟定期执行设备缓存更新
      */
-    Long CACHE_UPDATE_INTERVAL = 1 * 60 * 1000L;
+    Long CACHE_UPDATE_INTERVAL = 5 * 60 * 1000L;
 
     /**
      * influxDB default bucketName
@@ -55,15 +55,21 @@ public interface JfcloudColdChainConstants {
     int RETRY_DELAY_MS = 5000;
 
     /**
-     * 消息推送限制固定时间内的最大推送次数,10分钟内推送2
+     * 消息推送限制固定时间内的最大推送次数,5分钟内推送1
      */
     int MESS_PUSH_MAX_PUSH_COUNT = 1;
     /**
-     * 消息推送限制的时间窗口,单位为毫秒(10分钟)
+     * 消息推送限制的时间窗口,单位为毫秒(5分钟)
      */
-    long MESS_PUSH_TIME_WINDOW = 10 * 60 * 1000;
+    long MESS_PUSH_TIME_WINDOW = 5 * 60 * 1000;
     /**
      * redis 缓存用户推送消息频率限制
      */
     String REDIS_PUSH_HISTORY_KEY_PREFIX = "alarm_push_history";
+
+    /**
+     * 每 5 分钟定期执行设备缓存更新
+     */
+    Long OFFLINE_THRESHOLD_SECONDS = 5 * 60 * 1000L;
+
 }

+ 5 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/renke/config/JfcloudColdChainServerProperties.java

@@ -29,5 +29,10 @@ public class JfcloudColdChainServerProperties {
      */
     private Long cacheUpdateInterval = JfcloudColdChainConstants.CACHE_UPDATE_INTERVAL;
 
+    /**
+     * 每 5 分钟定期执行设备在线检测
+     */
+    private Long deviceOfflineInterval = JfcloudColdChainConstants.OFFLINE_THRESHOLD_SECONDS;
+
 
 }

+ 3 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/push/param/PushParam.java

@@ -42,4 +42,7 @@ public class PushParam {
     public PushParam(String value) {
         this.value = value;
     }
+
+    public PushParam() {
+    }
 }