JAVA 对接支持远程通信的智能电表(DLT645) / 智能水表(RS485,MODBUS)指南

一、此文目的

  1. 适用于刚接触智能水电表对接的人员,几十、几百甚至几千台设备的不成问题,但几十万设备的对接尚未涉及。
  2. 为刚接触此类对接场景的小伙伴提供整体思路及注意点,虽未贴全部代码,但核心代码已到位。

二、涉及问题

(一)协议不同

  • 设备通信并非 Java Web 的 HTTP 协议。HTTP 基于 TCP/IP,TCP/IP 可视为 HTTP 上层协议。
  • 支持 TCP/IP 协议的设备可借助 Netty 对接 Java Web 项目,Maven 集成 Netty 方法可自行百度。

(二)端口配置

  • Java 容器启动时,Netty 监听的端口随之启动,可监听多个端口,如水表可用 8081 端口,电表可用 8082 端口。
  • 访问 Java Web 项目地址为 192.160.0.22:8080 时,电表的通信模组配置地址为 192.160.0.22:8082。
  • 调试时可下载 NetAssist.exe,设置为 TCP 服务器或客户端。

(三)如何通信

  • 智能电表因内置支持 4G/5G 远程通信的模组而具备远程通信功能。
  • 水电表通信模组需配置 Java 程序部署的服务器地址加 Netty 监听的端口(非 Java 项目启动端口)。
  • 通过 TCP/IP 方式通信,硬件设备通信模组可设置 HEX(16 进制)或 ASCII(10 进制)与 Netty 交互,16 进制字节码对应 Java 里的 ByteBuff,ASCII 对应 String。
  • 启动 Netty 监听后,Java 端为服务器端,每个电表为客户端,Netty 中的每个 channel 是与每个设备通信后的渠道产物。

(四)模式选择
        本文选择 tcp 透传方式,也可选择其他方式如 MQTT等。

(五)数据获取方式

  • 当客户端与服务端建立连接后,客户端会向服务端发送一条消息,内容为 SN (设备唯一识别码),以告知服务端 “我是谁”,服务端便知道需给哪个设备发消息以及这是哪个设备的响应。
  • 数据编码:需注意客户端设置的是 HEX 还是 ASCII。DLT645 协议响应的数据可参考:FE FE FE FE 68 62 01 76 00 00 81 68 11 04 33 33 34 33 0C 16。每一帧数据都有具体含义,因篇幅所限不详细列举。
  • 厂家一般会提供 PDF/WORD 版本的协议文件,也会有相应的对接人员。
  • 调试:有两种办法。第一种是将电表设备放到 PC 旁边,接一堆线(如 220V 以及低压线),相当复杂且危险,调试效率低。第二种是将写好的程序部署到支持外网的机器上,把相应的 IP+Netty 监听的端口提供给客户,让他们配置进去,远程调试。
  • 由于写好的代码需要部署至服务器,不能频繁发布频繁测试,效率太低。此时可下载 NetAssist.exe,用它来模拟客户端,本地服务启动后就是服务端,效率极高,先保证自测通过再部署至服务器,减少发布频次,提高效率。这是在快完成时才发现的,之前踩坑无数。

(六)上代码

6.1 netty 服务器(主要用于与客户端通讯)

package com.ape.service.netty;

import com.ape.module.netty.NettyServerHandlerEle;
import com.ape.module.netty.NettyServerHandlerWaterEmf;
import com.ape.module.netty.NettyServerHandlerWaterUw;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * netty服务器,主要用于与客户端通讯
 */
@Slf4j
public class NettyServer {

    //监听端口-电表
    private final int elePort;

    //监听端口-水表-电磁流量计
    private final int waterEmfPort;

    //监听端口-水表-超声波流量计
    private final int waterUwPort;


    public NettyServer(int elePort, int waterEmfPort, int waterUwPort) {
        this.elePort = elePort;
        this.waterUwPort = waterUwPort;
        this.waterEmfPort = waterEmfPort;
    }

    /**
     * 编写run方法,处理客户端的请求
     *
     * @throws Exception
     */
    public void run() throws Exception {

        //创建两个线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        //8个NioEventLoop
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            //电表
            ServerBootstrap serverBootstrapEle = new ServerBootstrap();
            //[水表]电磁流量计
            ServerBootstrap serverBootstrapWaterEmf = new ServerBootstrap();
            //[水表]超声波
            ServerBootstrap serverBootstrapWaterUw = new ServerBootstrap();

            serverBootstrapEle.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            //获取到pipeline
                            ChannelPipeline pipeline = ch.pipeline();
                            //向pipeline加入心跳超时机制
                            pipeline.addLast(new IdleStateHandler(45, 0, 0, TimeUnit.SECONDS));
//                            //向pipeline加入解码器
                            pipeline.addLast("decoder", new StringDecoder());
//                            //向pipeline加入编码器
                            pipeline.addLast("encoder", new StringEncoder());
                            //加入自己的业务处理handler
                            pipeline.addLast(new NettyServerHandlerEle());

                        }
                    });

            serverBootstrapWaterEmf.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //获取到pipeline
                            ChannelPipeline pipeline = ch.pipeline();
                            //向pipeline加入心跳超时机制
                            pipeline.addLast(new IdleStateHandler(45, 0, 0, TimeUnit.SECONDS));
//                            //向pipeline加入解码器
                            pipeline.addLast("decoder", new StringDecoder());
//                            //向pipeline加入编码器
                            pipeline.addLast("encoder", new StringEncoder());
                            //加入自己的业务处理handler
                            pipeline.addLast(new NettyServerHandlerWaterEmf());

                        }
                    });

            serverBootstrapWaterUw.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //获取到pipeline
                            ChannelPipeline pipeline = ch.pipeline();
                            //向pipeline加入心跳超时机制
                            pipeline.addLast(new IdleStateHandler(45, 0, 0, TimeUnit.SECONDS));
//                            //向pipeline加入解码器
                            pipeline.addLast("decoder", new StringDecoder());
//                            //向pipeline加入编码器
                            pipeline.addLast("encoder", new StringEncoder());
                            //加入自己的业务处理handler
                            pipeline.addLast(new NettyServerHandlerWaterUw());

                        }
                    });

            ChannelFuture channelFutureEle = serverBootstrapEle.bind(elePort).sync();
            log.info("netty服务器启动成功-电表tcp端口:{}", elePort);
            ChannelFuture channelFutureWaterEmf = serverBootstrapWaterEmf.bind(waterEmfPort).sync();
            log.info("netty服务器启动成功-水表[电磁流量计]tcp端口:{}", waterEmfPort);
            ChannelFuture channelFutureWaterUw = serverBootstrapWaterUw.bind(waterUwPort).sync();
            log.info("netty服务器启动成功-水表[超声波流量计]tcp端口:{}", waterUwPort);
            //监听关闭
            channelFutureEle.channel().closeFuture().sync();
            channelFutureWaterEmf.channel().closeFuture().sync();
            channelFutureWaterUw.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

}

  注意以下代码,如果是 HEX数据格式,则要把这两行注释掉。

//                            //向pipeline加入解码器
                            pipeline.addLast("decoder", new StringDecoder());
//                            //向pipeline加入编码器
                            pipeline.addLast("encoder", new StringEncoder());
  • 6.2 启动 Netty

  • package com.ape;
    
    import com.ape.service.netty.NettyServer;
    import org.apache.tomcat.util.http.LegacyCookieProcessor;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
    import org.springframework.boot.web.server.WebServerFactoryCustomizer;
    import org.springframework.boot.web.servlet.MultipartConfigFactory;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.EnableAsync;
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.stereotype.Indexed;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    import org.springframework.util.unit.DataSize;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.servlet.MultipartConfigElement;
    import java.util.TimeZone;
    
    @Indexed
    @SpringBootApplication(scanBasePackages = {"com.ape"})
    @MapperScan(basePackages = {"com.ape.mapper.*"})
    @RestController
    @EnableScheduling
    @EnableAsync
    @EnableTransactionManagement
    @Configuration
    public class PowerApplication implements CommandLineRunner {
    
        @Value("${tcp.elePort}")
        private Integer elePort;
    
        @Value("${tcp.waterEmfPort}")
        private Integer waterEmfPort;
    
        @Value("${tcp.waterUwPort}")
        private Integer waterUwPort;
    
        public static void main(String[] args) {
            TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
            SpringApplication.run(PowerApplication.class);
        }
    
        @Override
        public void run(String... args) throws Exception {
            new Thread(() -> {
                try {
                    new NettyServer(elePort, waterEmfPort, waterUwPort).run();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
    
  • 6.3 NettyServer 控制器

    6.3.1 类声明的泛型
    如果是 HEX 格式,则类声明中的泛型为ByteBuffer;如果是 ASCII 格式,则为String

    6.3.2 响应入口
    channelRead0为真正的响应入口,请注意阅读。

    6.3.3 注册包说明
    以下类里多个地方出现 “注册包”字样,此为智能电表每次和 SERVER 端建立连接后发送的设备的 SN(可以理解为设备唯一识别码,只会发送一次哈,切记),用来告知程序这是哪个设备和你建立的连接。一个连接就是一个channel。每次发送以及响应时就知道是和谁在进行交互。

  • package com.ape.module.netty;
    
    import com.ape.common.utils.StringUtil;
    import com.ape.common.utils.StringUtils;
    import com.ape.pojo.dto.power.EleAcquisitionDTO;
    import com.ape.pojo.vo.power.EleAcquisitionVO;
    import com.ape.pojo.vo.power.EleEquipVO;
    import com.ape.service.power.EleAcquisitionService;
    import com.ape.service.power.EleEquipService;
    import io.netty.channel.Channel;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    import io.netty.channel.group.ChannelGroup;
    import io.netty.channel.group.DefaultChannelGroup;
    import io.netty.handler.timeout.IdleState;
    import io.netty.handler.timeout.IdleStateEvent;
    import io.netty.util.concurrent.GlobalEventExecutor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.PostConstruct;
    import java.math.BigDecimal;
    import java.nio.ByteBuffer;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 电表
     *
     * @author hwlva
     */
    @Slf4j
    @Component
    public class NettyServerHandlerEle extends SimpleChannelInboundHandler<ByteBuffer> {
    
        /**
         * 定义本类的静态对象
         */
        private static NettyServerHandlerEle nettyServerHandlerEle;
    
        /**
         * 3. 添加  @PostConstruct 注解 自定义初始化方法
         */
        @PostConstruct
        public void init() {
            nettyServerHandlerEle = this;
        }
    
        /**
         * 定义一个channle 组,管理所有的channel
         * GlobalEventExecutor.INSTANCE) 是全局的事件执行器,是一个单例
         */
        public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    
        /**
         * 使用一个Map来存储每个通道的变量,key为通道,value为 SN
         */
        public static Map<Channel, String> variables = new HashMap<>();
    
        /**
         * 客户端是否是第一次给服务器发送消息标识
         */
        private boolean firstMessageReceived = false;
    
        @Value("${ele.heartContent}")
        private String heartContent;
    
        @Value("${ele.idleData}")
        private String idleData;
    
        @Autowired
        private EleEquipService eleEquipService;
    
    
        @Autowired
        EleAcquisitionService eleAcquisitionService;
    
        /**
         * 有客户端与服务器发生连接时执行此方法
         * 1.打印提示信息
         * 2.将客户端保存到 channelGroup 中
         */
        @Override
        public void handlerAdded(ChannelHandlerContext ctx) {
            Channel channel = ctx.channel();
            log.info("[电表]有新的客户端与服务器发生连接。客户端地址:{}", channel.remoteAddress().toString());
            channelGroup.add(channel);
        }
    
        /**
         * 当有客户端与服务器断开连接时执行此方法,此时会自动将此客户端从 channelGroup 中移除
         * 1.打印提示信息
         */
        @Override
        public void handlerRemoved(ChannelHandlerContext ctx) {
            Channel channel = ctx.channel();
            log.error("[电表]有客户端与服务器断开连接。客户端地址:{},sn:{}", channel.remoteAddress().toString(), getVariable(channel));
            doErrorHandle(ctx);
            removeVariable(channel);
            logMethod();
        }
    
        /**
         * 超时处理,如果45秒没有收到客户端的心跳,就触发; 如果超过两次,则直接关闭;
         */
        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object obj) throws Exception {
            log.info("[电表] userEventTriggered..........");
            if (obj instanceof IdleStateEvent) {
                IdleStateEvent event = (IdleStateEvent) obj;
                // 如果读通道处于空闲状态,说明没有接收到心跳命令
                if (IdleState.READER_IDLE.equals(event.state())) {
                    log.error("[电表]超过45秒未收到客户端消息,关闭连接,通道标识:{},sn:{}", ctx.channel().remoteAddress().toString(), getVariable(ctx.channel()));
                    ctx.channel().close();
                    doErrorHandle(ctx);
                    removeVariable(ctx.channel());
                }
            } else {
                super.userEventTriggered(ctx, obj);
            }
        }
    
        /**
         * 表示channel 处于活动状态
         */
        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            log.info("[电表]channelActive.........." + ctx.channel().remoteAddress());
        }
    
        /**
         * 表示channel 处于不活动状态
         */
        @Override
        public void channelInactive(ChannelHandlerContext ctx) {
            log.info("[电表]channelInactive..........");
            Channel channel = ctx.channel();
            log.error(channel.remoteAddress() + " 处于不活动状态");
        }
    
        /**
         * 读取到客户端发来的数据数据
         */
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String stringMsg) {
            log.info("---[电表] channelRead0 start...StringMsg:{},sn:{}---", stringMsg, getVariable(ctx.channel()));
            //第一次收到的消息为注册码即SN
            if (!firstMessageReceived) {
                // 这是第一次接收到的消息
                log.info("[电表]第一次接收到的注册包: " + stringMsg);
                String sn = getVariable(ctx.channel());
                if (StringUtils.isEmpty(sn)) {
                    setVariable(ctx.channel(), stringMsg);
                }
                firstMessageReceived = true;
            } else if (stringMsg.equals(nettyServerHandlerEle.heartContent)) {
                log.info("[电表]消息类型为 [心跳],[{}]", stringMsg);
            } else {
                log.info("[电表]消息类型为 非 心跳,{}", stringMsg);
                //获取有功和无功的读数
                BigDecimal res = StringUtil.analyzingConsumption(stringMsg);
                EleEquipVO eleEquipVO = nettyServerHandlerEle.eleEquipService.getBySn(getVariable(ctx.channel()));
                String equipName = eleEquipVO.getName();
                String deviceNo = eleEquipVO.getDeviceNo();
                //判断类型
                String tempMsg = stringMsg.replaceAll(" ", "");
                String sb = tempMsg.substring(28, 36);
                String idleOne = nettyServerHandlerEle.idleData.replaceAll(" ", "");
    //            String idleOne = "33333633";
                EleAcquisitionDTO eleAcquisitionDTO = new EleAcquisitionDTO();
                //根据设备id获去最大的值
                EleAcquisitionVO acquisitionVO = nettyServerHandlerEle.eleAcquisitionService.getMaxDataByDeviceNo(deviceNo);
                if (idleOne.equals(sb)) {
                    //获取最新的一条采集数据
                    EleAcquisitionVO eleAcquisitionVO = nettyServerHandlerEle.eleAcquisitionService.selectByDeviceNoOrNowTime(deviceNo);
                    //判断有没有采集的数据 有采集的数据 则修改最新的一条数据的无功读数
                    if (eleAcquisitionVO != null) {
                        eleAcquisitionDTO.setId(eleAcquisitionVO.getId());
                        eleAcquisitionDTO.setFieldValue(eleAcquisitionVO.getFieldValue());
                        eleAcquisitionDTO.setActualValue(eleAcquisitionVO.getActualValue());
                    }else {
                        //没有则新增,把有功读数和有功实际读数的最大值给塞进去进行新增
                        eleAcquisitionDTO.setFieldValue(acquisitionVO.getFieldValue());
                        eleAcquisitionDTO.setActualValue(acquisitionVO.getActualValue());
                    }
                    eleAcquisitionDTO.setIdleFieldValue(res);
                    //计算倍数
                    BigDecimal counts = countMultiple(eleEquipVO, res);
                    eleAcquisitionDTO.setIdleActualValue(counts);
                } else {
                    eleAcquisitionDTO.setFieldValue(res);
                    //计算倍数
                    BigDecimal counts = countMultiple(eleEquipVO, res);
                    eleAcquisitionDTO.setActualValue(counts);
                    eleAcquisitionDTO.setIdleFieldValue(acquisitionVO.getIdleFieldValue());
                    eleAcquisitionDTO.setIdleActualValue(acquisitionVO.getIdleActualValue());
                }
                eleAcquisitionDTO.setDeviceNo(deviceNo);
                log.info("[电表]消息类型为 非 心跳,内容是:{},设备名称为:{},读取值为:{}kwh", stringMsg, equipName, res);
                eleAcquisitionDTO.setLocation(equipName);
                //保存电数据采集并且触发daily生成
                log.info("------------deviceno:{}", deviceNo);
                nettyServerHandlerEle.eleAcquisitionService.createAcqAndDaily(eleAcquisitionDTO);
            }
            logMethod();
            log.info("-------------------[电表] channelRead0 end..........");
        }
    
        private static BigDecimal countMultiple(EleEquipVO eleEquipVO, BigDecimal res) {
            Integer multiple = eleEquipVO.getMultiple();
            //乘以倍数
            if (multiple != null && multiple > 0) {
                // 将double转换为BigDecimal
                BigDecimal numberAsBigDecimal = BigDecimal.valueOf(multiple);
                BigDecimal actual = numberAsBigDecimal.multiply(res);
                return actual;
            } else {
                return res;
            }
        }
    
        /**
         * 处理异常
         */
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            log.error("[电表]发生异常。异常信息:{},sn:{}", cause.getMessage() + cause.getCause() + ctx.channel().remoteAddress().toString(), getVariable(ctx.channel()));
            doErrorHandle(ctx);
            //关闭通道
            ctx.close();
            removeVariable(ctx.channel());
        }
    
        private void doErrorHandle(ChannelHandlerContext ctx) {
            log.error("[电表]doErrorHandle。异常信息:{},sn:{}", ctx.channel().remoteAddress(), getVariable(ctx.channel()));
        }
    
        // 存储变量的方法
        public static void setVariable(Channel ctx, String variable) {
            variables.put(ctx, variable);
        }
    
        // 获取变量的方法
        public static String getVariable(Channel ctx) {
            return variables.get(ctx);
        }
    
        /**
         * 删除ctx
         *
         * @param ctx
         */
        public static void removeVariable(Channel ctx) {
            if (StringUtils.isNotEmpty(variables.get(ctx))) {
                variables.remove(ctx);
            }
        }
    
        public static void logMethod() {
            log.info("[电表]客户端连接总数量为:{}", channelGroup.size());
            log.info("[电表]ctx和SN关系map长度为,{}", variables.size());
            variables.forEach((key, value) -> log.info("[电表]" + key.remoteAddress() + ":" + value));
        }
    
    
    }
  • 6.4 向客户端发送指令以达到采集的目的
    发送的入口方法为 sendMessage。

    package com.ape.service.power.impl;
    
    import cn.hutool.core.bean.BeanUtil;
    import com.ape.common.base.LocalUser;
    import com.ape.common.base.Response;
    import com.ape.common.cache.ParamsCacheUtil;
    import com.ape.common.compont.AsyncDayInfoDailyComponent;
    import com.ape.common.constant.SysConstants;
    import com.ape.common.enums.AlarmCodeEnum;
    import com.ape.common.enums.AlarmLevelEnum;
    import com.ape.common.enums.AlarmTypeEnum;
    import com.ape.common.enums.EquipStatusEnum;
    import com.ape.common.utils.*;
    import com.ape.mapper.power.EleAcquisitionMapper;
    import com.ape.module.netty.NettyServerHandlerEle;
    import com.ape.pojo.dto.power.EleAcquisitionDTO;
    import com.ape.pojo.entity.power.EleAcquisitionEntity;
    import com.ape.pojo.entity.power.EleAlarmRecordEntity;
    import com.ape.pojo.query.power.EleAcquisitionQuery;
    import com.ape.pojo.vo.power.EleAcquisitionVO;
    import com.ape.pojo.vo.power.EleEquipVO;
    import com.ape.pojo.vo.power.EleWarningStrategyVO;
    import com.ape.service.power.*;
    import com.baomidou.mybatisplus.core.metadata.IPage;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.google.common.collect.Lists;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.ObjectUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.math.BigDecimal;
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.*;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.stream.Collectors;
    
    /**
     * 电数据采集Service业务层处理
     *
     * @author yrw
     * @date 2024-04-15
     */
    @Slf4j
    @Service
    public class EleAcquisitionServiceImpl extends ServiceImpl<EleAcquisitionMapper, EleAcquisitionEntity> implements EleAcquisitionService {
    
        @Autowired
        private EleEquipService eleEquipService;
    
        @Autowired
        private AsyncDayInfoDailyComponent dayInfoDailyComponent;
    
        @Autowired
        private EleSystemDailyService eleSystemDailyService;
    
        @Autowired
        private EleAlarmRecordService eleAlarmRecordService;
    
        @Autowired
        private EleWarningStrategyService eleWarningStrategyService;
    
        @Value("${ele.prefix}")
        private String prefix;
    
        @Value("${ele.start}")
        private String start;
    
        @Value("${ele.control}")
        private String control;
    
        @Value("${ele.dataLength}")
        private String dataLength;
    
        @Value("${ele.data}")
        private String data;
    
        @Value("${ele.end}")
        private String end;
    
        @Value("${ele.idleData}")
        private String idleData;
    
        @Override
        @Transactional(rollbackFor = Exception.class)
        public Response create(EleAcquisitionDTO dto) {
            EleAcquisitionEntity entity = new EleAcquisitionEntity();
            BeanUtil.copyProperties(dto, entity);
            try {
                if (ObjectUtils.isEmpty(dto.getId())) {
                    this.baseMapper.insert(entity);
                } else {
                    this.baseMapper.updateById(entity);
                }
            } catch (Exception e) {
                log.error("保存出错", e);
                return Response.fromException(e);
            }
            return Response.success();
        }
    
        @Override
        public IPage<EleAcquisitionVO> search(EleAcquisitionQuery query) {
            Page<EleAcquisitionVO> pager = new Page<>(query.getCurrentPage(), query.getPageSize());
            return this.baseMapper.search(pager, query);
        }
    
        @Override
        public Response<EleAcquisitionVO> findById(String id) {
            return Response.success(this.baseMapper.findById(id));
        }
    
        @Override
        public Response deleteById(String id) {
            String userId = LocalUser.getLocalUser().getUserId();
            this.baseMapper.delById(id, userId);
            return Response.success();
        }
    
        //------ 设备相关接口  start -----------------
    
        //根据设备编号修改名称
        @Override
        public void updateByDeviceNO(String deviceNo, String name) {
            this.baseMapper.updateByDeviceNo(deviceNo, name);
        }
    
        //根据设备编号获取采集数据
        @Override
        public List<EleAcquisitionVO> selectByDeviceNo(String deviceNo) {
            return this.baseMapper.selectByDeviceNo(deviceNo);
        }
    
        //根据设备编号删除采集数据
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void deleteByDeviceNo(String deviceNo) {
            if (StringUtils.isBlank(deviceNo)) {
                new Exception();
            }
            this.baseMapper.deleteByDeviceNo(deviceNo);
        }
    
        //------ 设备相关接口  end -----------------
    
    
        //------ 定时任务相关接口  start -----------------
    
        //获取电表有功读数
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void acquisitionEleData() {
            //[电表]开始批量发送消息到电表客户端
            sendMessage();
            //更新电表状态(在线/离线)
            batchHandleEquipStatus();
        }
    
        // 批量发送消息到电表获取无功读数一
        @Override
        public void acquisitionEleIdleOneData() {
            //[电表]开始批量发送消息到电表客户端
            sendIdleMessage(idleData);
            //更新电表状态(在线/离线)
            batchHandleEquipStatus();
        }
    
        //根据设备编号获取最大的有功和无功
        @Override
        public EleAcquisitionVO getMaxDataByDeviceNo(String deviceNo) {
            return this.baseMapper.getMaxDataByDeviceNo(deviceNo);
        }
    
        //根据日期和设备编号获取最新一条采集信息
        @Override
        public EleAcquisitionVO selectByDeviceNoOrNowTime(String deviceNo) {
            return this.baseMapper.selectByDeviceNoOrNowTime(deviceNo);
        }
    
        //保存电数据采集并且触发daily生成
        @Override
        public void createAcqAndDaily(EleAcquisitionDTO dto) {
            //保存电数据采集
            this.create(dto);
            //触发daily生成
            eleSystemDailyService.saveByDeviceNo(dto.getDeviceNo());
        }
    
        //删除采集表中3个月前的数据
        @Override
        public void delAcqInfoMonth() {
            String month = ParamsCacheUtil.queryValue("del_acq_info_month", "del_acq_info_month");
            LocalDateTime time = LocalDateTime.now().minusMonths(Long.parseLong(month));
            this.baseMapper.deleteByTime(time.toString());
        }
    
        //定时任务已停用
        //根据日期制造虚假电采集数据
        @Override
        public Response setEleVirtualData(String date) {
            try {
                //判断传入的日期是否有值 没有则获取当前日期
                String dateTime = StringUtils.isBlank(date) ? DateUtil.getDatestr(new Date(), DateUtil.FORMAT_DATE) : date;
                // 要插入的数据列表
                List<EleAcquisitionEntity> addList = new ArrayList<>();
                // 1、查出现有电表 列表
                List<EleEquipVO> equipVOList = eleEquipService.getLists();
                // 2、循环电表列表 制造 每个电表 指定日期(当日)的 24条数据
                equipVOList.forEach(eleEquipVO -> {
                    // 查询当前电表的最终读数
                    BigDecimal maxValueByDeviceNo = this.baseMapper.findByDeviceNo(eleEquipVO.getDeviceNo());
                    if (maxValueByDeviceNo == null) {
                        maxValueByDeviceNo = new BigDecimal("0.000");
                    }
    
                    // 获取 24个上报时间点
                    List<String> todayByHour = DateUtil.getHoursInMonth(dateTime);
    
                    // 第一个随机读数使用表最终读数生成
                    BigDecimal bigDecimalRandom = RandomUtils.getBigDecimalRandom(maxValueByDeviceNo.add(new BigDecimal(SysConstants.ELE_VIRTUAL_DATA_RANDOM_START)), maxValueByDeviceNo.add(new BigDecimal(SysConstants.ELE_VIRTUAL_DATA_RANDOM_END)));
    
                    for (String time : todayByHour) {
                        // 获取24个上报时间点(00:00:00-23:00:00),每隔一小时一条
                        addList.add(new EleAcquisitionEntity(eleEquipVO, time, bigDecimalRandom));
    
                        // 剩余随机读数生成读数随机数
                        bigDecimalRandom = RandomUtils.getBigDecimalRandom(bigDecimalRandom.add(new BigDecimal(SysConstants.ELE_VIRTUAL_DATA_RANDOM_START)), bigDecimalRandom.add(new BigDecimal(SysConstants.ELE_VIRTUAL_DATA_RANDOM_END)));
                    }
                });
                //生成之前先删除所生成当天的数据
                this.baseMapper.deleteByReportedAt(dateTime);
                // 3、批量出入数据
                this.baseMapper.batchInsert(addList);
                return Response.success();
            } catch (Exception e) {
                log.error("用电定时任务批处理失败", e);
                return Response.fromException(e);
            }
    
        }
    
        //------ 定时任务相关接口  end -----------------
    
        //------ 定时任务相关方法  start -----------------
    
        /**
         * [电表]开始批量发送消息到电表客户端 获取有功读数
         */
        private void sendMessage() {
            Integer size = NettyServerHandlerEle.channelGroup.stream().filter(e -> e.isActive()).collect(Collectors.toList()).size();
            if (size <= 0) {
                log.info("[电表]没有连接的客户端(激活的channel)");
                return;
            }
            log.info("[电表]开始批量发送消息到电表客户端(激活的channel),{}个", size);
            AtomicInteger count = new AtomicInteger();
            NettyServerHandlerEle.channelGroup.forEach(channel -> {
                if (channel.isActive()) {
                    String sn = NettyServerHandlerEle.variables.get(channel);
                    if (StringUtil.isNotEmpty(sn)) {
                        EleEquipVO eleEquipVO = eleEquipService.getBySn(sn);
                        if (eleEquipVO != null && StringUtil.isNotEmpty(eleEquipVO.getDeviceNo())) {
                            String command = assembleCommand(eleEquipVO.getDeviceNo(), data);
                            command = StringUtil.replaceAllStr(command);
                            channel.writeAndFlush(command);
                            count.getAndIncrement();
                            log.info("[电表]开始给设备[{}]发送指令,指令为:{}", eleEquipVO.getName(), command);
                        } else {
                            log.info("[电表]没有SN={}的设备", sn);
                        }
                    } else {
                        log.info("[电表]{}未发送注册码,sn为空", channel.remoteAddress());
                    }
                }
            });
            log.info("[电表]总共给[{}]个设备发送了命令", count);
        }
    
        /**
         * [电表]开始批量发送消息到电表客户端 获取无功读数
         * @param sendMessage
         */
        private void sendIdleMessage(String sendMessage) {
            Integer size = NettyServerHandlerEle.channelGroup.stream().filter(e -> e.isActive()).collect(Collectors.toList()).size();
            if (size <= 0) {
                log.info("[电表]没有连接的客户端(激活的channel)");
                return;
            }
            log.info("[电表]开始批量发送消息到电表客户端(激活的channel),{}个", size);
            AtomicInteger count = new AtomicInteger();
            NettyServerHandlerEle.channelGroup.forEach(channel -> {
                if (channel.isActive()) {
                    String sn = NettyServerHandlerEle.variables.get(channel);
                    if (StringUtil.isNotEmpty(sn)) {
                        EleEquipVO eleEquipVO = eleEquipService.getBySn(sn);
                        if (eleEquipVO != null && StringUtil.isNotEmpty(eleEquipVO.getDeviceNo())) {
                            String command = assembleCommand(eleEquipVO.getDeviceNo(), sendMessage);
                            command = StringUtil.replaceAllStr(command);
                            channel.writeAndFlush(command);
                            count.getAndIncrement();
                            log.info("[电表]开始给设备[{}]发送无功指令,指令为:{}", eleEquipVO.getName(), command);
                        } else {
                            log.info("[电表]没有SN={}的设备", sn);
                        }
                    } else {
                        log.info("[电表]{}未发送注册码,sn为空", channel.remoteAddress());
                    }
                }
            });
            log.info("[电表]总共给[{}]个设备发送了命令", count);
        }
    
        /**
         * 组装发送的命令
         * @return
         */
        private String assembleCommand(String deviceNo, String dataDomain) {
            //计算校验码的值base
            String base = String.format("%s%s%s%s%s%s", start, assembleAddress(deviceNo), start, control, dataLength, StringUtil.replaceAllStr(dataDomain));
            String verifyCode = StringUtil.makeCheck(base);
            String result = String.format("%s %s %s %s", prefix, base, verifyCode, end);
            return result;
        }
    
        /**
         * 12位地址位的 两位倒序
         * @return
         */
        private String assembleAddress(String deviceNo) {
            return StringUtil.addressToString(StringUtil.stringToBytes((deviceNo)));
        }
    
        /**
         * 给设备发送消息+生成告警记录
         */
        private void batchHandleEquipStatus() {
            //更新设备状态(在线or离线),如果离线则触发
    //        batchUpdateEquipStatus();
            //生成告警记录
            batchCreateAlarmRecord();
        }
        /**
         * 生成告警记录(离线)
         */
        private void batchCreateAlarmRecord() {
            //先从缓存中获取所有的设备信息
            List<EleEquipVO> equipList = eleEquipService.getLists();
            //获取告警策略里面设备离线的策略信息
            EleWarningStrategyVO strategyVO = eleWarningStrategyService.getByCode(AlarmCodeEnum.OFFLINE_CODE.getCode());
            if (strategyVO == null) {
                return;
            }
            //离线次数
            Integer offLineNum = strategyVO.getOffLineNum();
            //获取tcp里面存在的设备sn
            Collection<String> snList = NettyServerHandlerEle.variables.values();
            //告警记录
            List<EleAlarmRecordEntity> offLineList = Lists.newArrayList();
            //设备离线和在线的ids List
            List<String> offLineByIds = Lists.newArrayList();
            List<String> onLineByIds = Lists.newArrayList();
            //设备离线次数的ids
            List<String> offLineIds = Lists.newArrayList();
            // 判断如果tcp里面有没有设备
            if (snList.isEmpty()) {
                equipList.stream().forEach(e -> {
                    //判断如果设备离线的次数大于预计离线的次数,则提示告警
                    if (offLineNum < e.getOffLineNum()) {
                        //添加告警记录信息
                        offLineList.add(getEleAlarmRecordEntity(e));
                        offLineByIds.add(e.getId());
                    }else {
                        onLineByIds.add(e.getId());
                    }
                    offLineIds.add(e.getId());
                });
            } else {
                equipList.stream().forEach(e -> {
                    if (!snList.contains(e.getSn())) {
                        if (offLineNum < e.getOffLineNum()) {
                            offLineList.add(getEleAlarmRecordEntity(e));
                            offLineByIds.add(e.getId());
                        }else {
                            onLineByIds.add(e.getId());
                        }
                        offLineIds.add(e.getId());
                    } else {
                        onLineByIds.add(e.getId());
                    }
                });
            }
            //修改离线次数
            eleEquipService.updateByIds(offLineIds);
            //修改设备状态
            eleEquipService.batchUpdateStatusByIds(onLineByIds, EquipStatusEnum.ONLINE.getCode());
            eleEquipService.batchUpdateStatusByIds(offLineByIds, EquipStatusEnum.OFFLINE.getCode());
            //如果没有要添加的告警记录 ,则不需要删除和添加
            if (!offLineList.isEmpty()) {
                List<String> recordDeviceNos = offLineList.stream().map(EleAlarmRecordEntity::getDeviceNo).collect(Collectors.toList());
                eleAlarmRecordService.deleteByDeviceNos(recordDeviceNos);
                eleAlarmRecordService.saveBatch(offLineList);
            }
    
        }
    
        /**
         * 构造getEleAlarmRecordEntity对象
         *
         * @param eleEquipVO
         * @return
         */
        private EleAlarmRecordEntity getEleAlarmRecordEntity(EleEquipVO eleEquipVO) {
            EleAlarmRecordEntity entity = new EleAlarmRecordEntity();
            entity.setId(SysUtil.getUuid());
            entity.setDeviceNo(eleEquipVO.getDeviceNo());
            entity.setType(AlarmTypeEnum.TYPE_ONE.getCode());
            String title = String.format("设备离线[%s]", eleEquipVO.getName());
            entity.setTitle(title);
            entity.setLevel(AlarmLevelEnum.LEVEL_ONE.getCode());
            String content = String.format("设备[%s]断开连接", eleEquipVO.getName());
            entity.setContent(content);
            entity.setUpdateTime(LocalDateTime.now());
            return entity;
        }
    
        //------ 定时任务相关方法  end -----------------
    
    
        //------ 用电记录相关接口  start -----------------
    
        //根据设备编号查询 初始,终止,当前耗电 的有功和无功读数
        @Override
        @Transactional(rollbackFor = Exception.class)
        public EleAcquisitionVO getByDeviceNo(String deviceNo) {
            return this.baseMapper.getByDeviceNo(deviceNo);
        }
    
        //获取昨日最小的有功读数,如果昨日没有数据,则获取小于昨日的最大有功读数
        @Override
        public BigDecimal getYesterdayOrDeviceNo(String deviceNo, String yesterday) {
            BigDecimal counts = this.baseMapper.getYesterdayOrDeviceNo(deviceNo, yesterday);
            if (counts == null) {
                BigDecimal maxCounts = this.baseMapper.getUnderYesterdayOrDeviceNo(deviceNo, yesterday);
                return maxCounts;
            }
            return counts;
        }
    
        //获取昨日最小的无功读数,如果昨日没有数据,则获取小于昨日的最大无功读数
        @Override
        public BigDecimal getYesterdayOrDeviceNoIdle(String deviceNo, String yesterday) {
            BigDecimal counts = this.baseMapper.getYesterdayOrDeviceNoIdle(deviceNo, yesterday);
            if (counts == null) {
                BigDecimal maxCounts = this.baseMapper.getUnderYesterdayOrDeviceNoIdle(deviceNo, yesterday);
                return maxCounts;
            }
            return counts;
        }
    
        //获取当天有功的最大 最小 和 耗电量  已停用
        @Override
        @Transactional(rollbackFor = Error.class)
        public List<EleAcquisitionVO> getYesterdayInfo(String day) {
            return this.baseMapper.getYesterdayInfo(day);
        }
    
        //------ 用电记录相关接口  end -----------------
    
        //批量发送报文
        @Override
        public Response sendMsg(String sendMsg, String sns) {
            AtomicInteger count = new AtomicInteger();
            NettyServerHandlerEle.channelGroup.forEach(channel -> {
                if (channel.isActive()) {
                    String sn = NettyServerHandlerEle.variables.get(channel);
                    if (StringUtil.isNotEmpty(sn)) {
                        EleEquipVO eleEquipVO = eleEquipService.getBySn(sn);
                        if (eleEquipVO != null && StringUtil.isNotEmpty(eleEquipVO.getDeviceNo())) {
                            String command = sendMsg;
                            command = StringUtil.replaceAllStr(command);
                            channel.writeAndFlush(command);
                            count.getAndIncrement();
                            log.info("[电表]开始给设备[{}]发送指令,指令为:{}", eleEquipVO.getName(), command);
                        } else {
                            log.info("[电表]没有SN={}的设备", sn);
                        }
                    } else {
                        log.info("[电表]{}未发送注册码,sn为空", channel.remoteAddress());
                    }
                }
            });
            log.info("[电表]总共给[{}]个设备发送了命令", count);
            return Response.success();
        }
    
        //批量添加初始采集表数据
        @Override
        public Response batSave(String deviceNos) {
            if (StringUtils.isBlank(deviceNos)) {
                return new Response(4000001, "设备编号不能为空");
            }
            List<EleAcquisitionEntity> entities = new ArrayList<>();
            String[] deviceNoList = deviceNos.split(",");
            List<String> list = Arrays.asList(deviceNoList);
            list.stream().forEach(e -> {
                EleAcquisitionEntity entity = new EleAcquisitionEntity();
                EleEquipVO vo = eleEquipService.getByDeviceNo(e);
                entity.setDeviceNo(e);
                entity.setLocation(vo.getName());
                entity.setFieldValue(new BigDecimal(0));
                entity.setActualValue(new BigDecimal(0));
                String time = DateUtil.minCurrDay(DateUtil.getNowByFormat(DateUtil.FORMAT_DATE));
                entity.setReportedAt(LocalDateTime.parse(time, DateTimeFormatter.ofPattern(DateUtil.FORMAT_DATE_TIME)));
                entities.add(entity);
            });
            this.saveBatch(entities);
            return Response.success();
        }
    
        //批量添加采集数据
        @Override
        public void saveDayInfoForStartOrEnd(String startDay, String endDay) {
            try {
                List<String> days = DateUtil.getDaysForStartOrEnd(startDay, endDay);
                List<List<String>> partition = Lists.partition(days, 5);
                CountDownLatch countDownLatch = new CountDownLatch(partition.size());
                partition.stream().forEach(e -> dayInfoDailyComponent.saveDayInfoAcquisition(e, countDownLatch));
                countDownLatch.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
    }
    
  • (七)写在最后

    以上为核心代码块,最重要的接受和发送代码块集中的6.3和6.4里,如有需要仔细阅读,核心代码已贴出,如果有错误的地方还请指正。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值