Sfoglia il codice sorgente

采集模块包路径调整

learshaw 2 giorni fa
parent
commit
bcffcfffc5
95 ha cambiato i file con 12891 aggiunte e 84 eliminazioni
  1. 7 0
      ems/ems-cloud/ems-dev-adapter/pom.xml
  2. 2 2
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ClientInit.java
  3. 40 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/config/AcrelConfig.java
  4. 77 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/controller/AcrelElecMonitorController.java
  5. 440 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/handle/AcrelElecMonitorHandler.java
  6. 170 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/AlarmEventLog.java
  7. 103 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/Circuit.java
  8. 31 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/ConsumeReport.java
  9. 38 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/EnergyValue.java
  10. 68 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/MeterKeyValue.java
  11. 136 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/MeterUse.java
  12. 40 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/ReportNode.java
  13. 424 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/Station.java
  14. 35 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/StationEpiTime.java
  15. 45 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/StationType.java
  16. 35 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/Token.java
  17. 1 1
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/retrofit/Acrel3000Api.java
  18. 399 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/retrofit/Acrel3000Template.java
  19. 45 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/config/BaCtlConfig.java
  20. 77 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/controller/BaCtlController.java
  21. 1191 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/handle/BaCtlHandler.java
  22. 31 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/model/CodesVal.java
  23. 42 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/model/CodesValReq.java
  24. 42 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/model/CodesValSetReq.java
  25. 57 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/model/Token.java
  26. 76 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/retrofit/BaCtlEnergyApi.java
  27. 252 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/retrofit/BaCtlEnergyTemplate.java
  28. 164 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/controller/ChargingController.java
  29. 1 1
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/core/ChargingPileChannelInitializer.java
  30. 428 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/handle/ChargingHandler.java
  31. 302 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/handle/ChargingPileMessageHandler.java
  32. 278 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/TaskClacService.java
  33. 249 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/TaskColExecutor.java
  34. 83 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/AnalysisConfig.java
  35. 83 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/AsyncConfig.java
  36. 48 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/DateAttrConfig.java
  37. 35 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/EmsConfig.java
  38. 39 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/MqttConfig.java
  39. 47 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/MqttServerProperty.java
  40. 69 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/ThreadConfig.java
  41. 70 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/WeatherConfig.java
  42. 234 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/handle/BaseDevHandler.java
  43. 396 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/handle/BaseMeterDevHandler.java
  44. 85 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/handle/RootMsgHandler.java
  45. 66 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mock/MockGccController.java
  46. 66 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mock/MockGczrController.java
  47. 66 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mock/MockHmController.java
  48. 66 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mock/MockLjcyController.java
  49. 216 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mock/MockPileController.java
  50. 264 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mock/TestController.java
  51. 123 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/model/CallData.java
  52. 28 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mqtt/MqttMessageHandler.java
  53. 145 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mqtt/MqttTemplate.java
  54. 53 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/retrofit/BaseApiTemplate.java
  55. 87 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/retrofit/EmsApi.java
  56. 348 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/retrofit/EmsApiTemplate.java
  57. 86 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/geekopen/MqttCacheMsg.java
  58. 65 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/geekopen/ObjectCache.java
  59. 77 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/geekopen/controller/CircuitBreakerController.java
  60. 433 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/geekopen/handle/GeekOpenCbHandler.java
  61. 37 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/growatt/config/GrowattConfig.java
  62. 77 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/growatt/controller/GrowattController.java
  63. 787 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/growatt/handle/GrowattHandler.java
  64. 43 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/growatt/model/DataLogger.java
  65. 51 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/growatt/model/DeviceInfo.java
  66. 66 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/growatt/retrofit/GrowattApi.java
  67. 213 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/growatt/retrofit/GrowattTemplate.java
  68. 83 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/keka/controller/ButtonSwitchController.java
  69. 615 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/keka/handle/Keka86BsHandler.java
  70. 59 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/keka/model/DeviceStatus.java
  71. 38 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/keka/model/ModbusCommand.java
  72. 54 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/config/SquareLightCtlConfig.java
  73. 78 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/controller/SquareLightCtlController.java
  74. 735 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/handle/SquareLightCtlHandler.java
  75. 40 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/AppLoginReq.java
  76. 46 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/AppLoginRes.java
  77. 32 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/BaselineInfo.java
  78. 40 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/CcElecInfo.java
  79. 36 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/DevSub.java
  80. 32 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/DeviceInfo.java
  81. 45 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/LightControlReq.java
  82. 55 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/SlcDevice.java
  83. 88 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/SlcDeviceLamp.java
  84. 42 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/SlcProject.java
  85. 29 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/SlcProjectSubset.java
  86. 35 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/SlcResponse.java
  87. 37 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/StatusInfo.java
  88. 36 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/SyncStatusReq.java
  89. 130 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/retrofit/SquareLightCtlApi.java
  90. 366 0
      ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/retrofit/SquareLightCtlTemplate.java
  91. 134 51
      ems/ems-cloud/ems-dev-adapter/src/main/resources/logback.xml
  92. 9 9
      ems/ems-cloud/ems-dev-adapter/src/test/java/com/huashe/test/Acrel3000Test.java
  93. 6 6
      ems/ems-cloud/ems-dev-adapter/src/test/java/com/huashe/test/BaCtlTest.java
  94. 4 5
      ems/ems-cloud/ems-dev-adapter/src/test/java/com/huashe/test/GrowattTest.java
  95. 9 9
      ems/ems-cloud/ems-dev-adapter/src/test/java/com/huashe/test/SquareLightCtlTest.java

+ 7 - 0
ems/ems-cloud/ems-dev-adapter/pom.xml

@@ -140,6 +140,13 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <skipTests>true</skipTests>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 

+ 2 - 2
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ClientInit.java

@@ -10,8 +10,8 @@
  */
 package com.ruoyi.ems;
 
-import com.ruoyi.ems.core.MqttTemplate;
-import com.ruoyi.ems.handle.RootMsgHandler;
+import com.ruoyi.ems.common.mqtt.MqttTemplate;
+import com.ruoyi.ems.common.handle.RootMsgHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Qualifier;

+ 40 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/config/AcrelConfig.java

@@ -0,0 +1,40 @@
+/*
+ * 文 件 名:  AcrelConfig
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/21
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.acrel.config;
+
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 安科瑞参数
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/21]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+@Configuration
+public class AcrelConfig {
+    @Value("${adapter.acrel.url}")
+    private String url;
+
+    @Value("${adapter.acrel.auth.loginName}")
+    private String authLoginName;
+
+    @Value("${adapter.acrel.auth.password}")
+    private String authPassword;
+
+    @Value("${adapter.acrel.subids}")
+    private String subIds;
+}

+ 77 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/controller/AcrelElecMonitorController.java

@@ -0,0 +1,77 @@
+/*
+ * 文 件 名:  AcrelElecMonitorController
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/10/17
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.acrel.controller;
+
+import com.ruoyi.ems.acrel.handle.AcrelElecMonitorHandler;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * 安科瑞电力监控控制类
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/10/17]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@RestController
+@CrossOrigin(allowedHeaders = "*", allowCredentials = "false")
+@RequestMapping("/elec-monitor-acrel")
+@Api(value = "AcrelElecMonitorController", description = "电力监控Api")
+public class AcrelElecMonitorController {
+    /**
+     * 日志
+     */
+    private static final Logger log = LoggerFactory.getLogger(AcrelElecMonitorController.class);
+
+    @Qualifier("acrelElecMonitorHandler")
+    @Resource
+    private AcrelElecMonitorHandler acrelElecMonitorHandler;
+
+    /**
+     * 常泰室内能耗能力调用
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/ct/abilityCall", method = RequestMethod.POST)
+    @ApiOperation(value = "/ct/abilityCall", notes = "电力监控能力调用")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public CallResponse<Void> ctElecMonitoCall(@RequestBody AbilityPayload abilityPayload) {
+        CallResponse<Void> res = null;
+
+        try {
+            res = acrelElecMonitorHandler.call(abilityPayload);
+        }
+        catch (Exception e) {
+            log.error("ctElecMonitoCall fail!", e);
+            res = new CallResponse<>(501, "内部错误:" + e.getMessage());
+        }
+
+        return res;
+    }
+}

+ 440 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/handle/AcrelElecMonitorHandler.java

@@ -0,0 +1,440 @@
+/*
+ * 文 件 名:  AcrelElecMonitorHandler
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/10/20
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.acrel.handle;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.ems.acrel.config.AcrelConfig;
+import com.ruoyi.ems.acrel.retrofit.Acrel3000Template;
+import com.ruoyi.ems.domain.EmsDevice;
+import com.ruoyi.ems.domain.EmsObjAttr;
+import com.ruoyi.ems.domain.EmsObjAttrValue;
+import com.ruoyi.ems.domain.MeterDevice;
+import com.ruoyi.ems.enums.DevObjType;
+import com.ruoyi.ems.common.handle.BaseMeterDevHandler;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import com.ruoyi.ems.model.QueryDevice;
+import com.ruoyi.ems.acrel.model.Circuit;
+import com.ruoyi.ems.acrel.model.MeterKeyValue;
+import com.ruoyi.ems.acrel.model.MeterUse;
+import com.ruoyi.ems.service.IEmsObjAttrService;
+import com.ruoyi.ems.util.PingUtils;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 安科瑞电力监控模块handle
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/10/20]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Service
+public class AcrelElecMonitorHandler extends BaseMeterDevHandler {
+    private static final Logger log = LoggerFactory.getLogger(AcrelElecMonitorHandler.class);
+
+    // 对接系统
+    private static final String SYS_MODE_CODE = "M_W4_SYS_ELEC_MONITOR";
+
+    // 保护装置模型代码
+    private static final String MONITOR_BHZZ_MODE_CODE = "M_W4_DEV_ELEC_MONITOR_BHZZ";
+
+    // 电表模型代码
+    private static final String MONITOR_DB_MODE_CODE = "M_W4_DEV_ELEC_MONITOR_DB";
+
+    // 设备子系统代码
+    private static final String SUBSYSTEM_CODE = "SYS_DLJK";
+
+    @Resource
+    private AcrelConfig config;
+
+    @Autowired
+    private IEmsObjAttrService objAttrService;
+
+    @Override
+    public List<MeterDevice> getMeterDeviceList() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public List<EmsDevice> getDeviceList() {
+        QueryDevice queryDevice = new QueryDevice();
+        queryDevice.setSubsystemCode(SUBSYSTEM_CODE);
+        return deviceService.selectList(queryDevice);
+    }
+
+    @Override
+    public CallResponse<Void> call(AbilityPayload abilityParam) {
+        CallResponse<Void> callResponse = null;
+
+        try {
+            if (DevObjType.SYSTEM.getCode() == abilityParam.getObjType()) {
+                switch (abilityParam.getAbilityKey()) {
+                    case "GetDeviceList":
+                        CompletableFuture.runAsync(this::execGetDeviceList);
+                        break;
+                    case "SyncDevAttrAll":
+                        CompletableFuture.runAsync(this::execSyncDevAttrAll);
+                        break;
+                    default:
+                        throw new UnsupportedOperationException("不支持的abilityKey");
+                }
+            }
+            else if (DevObjType.DEVC.getCode() == abilityParam.getObjType()) {
+                switch (abilityParam.getAbilityKey()) {
+                    case "SyncDevAttr":
+                        execSyncDevAttr(abilityParam.getObjCode(), abilityParam.getModelCode());
+                        break;
+                    default:
+                        throw new UnsupportedOperationException("不支持的abilityKey");
+                }
+            }
+
+            callResponse = new CallResponse<>(0, "成功");
+        }
+        catch (Exception e) {
+            callResponse = new CallResponse<>(-1, e.getMessage());
+            log.error("调用ability异常", e);
+        }
+
+        return callResponse;
+    }
+
+    @Override
+    public void refreshOnline() {
+        try {
+            // 1. 从URL中提取IP地址
+            String url = config.getUrl();
+            if (StringUtils.isBlank(url)) {
+                log.warn("电力监控系统URL配置为空,跳过心跳检测");
+                return;
+            }
+
+            String ipAddress = PingUtils.extractIpFromUrl(url);
+            if (StringUtils.isBlank(ipAddress)) {
+                log.warn("无法从URL中提取IP地址: {}", url);
+                return;
+            }
+
+            // 2. 执行ping检测
+            boolean isOnline = PingUtils.pingHost(ipAddress, 3000); // 3秒超时
+
+            // 3. 获取当前系统状态
+            String statusKey = "interfaceStatus";
+
+            // 查询当前状态
+            EmsObjAttrValue currentStatus = objAttrValueService.selectObjAttrValue(SYS_MODE_CODE, SUBSYSTEM_CODE,
+                statusKey);
+
+            String newStatus = isOnline ? "1" : "0";
+            String oldStatus = currentStatus != null ? currentStatus.getAttrValue() : null;
+
+            // 4. 状态发生变化时更新并记录
+            if (!StringUtils.equals(oldStatus, newStatus)) {
+                log.info("电力监控系统连接状态变化: {} -> {}, IP: {}", oldStatus, newStatus, ipAddress);
+
+                // 更新系统属性状态
+                objAttrValueService.updateObjAttrValue(SYS_MODE_CODE, SUBSYSTEM_CODE, statusKey, newStatus);
+
+                String key = isOnline ? "connect" : "disconnect";
+                triggerEvent(SYS_MODE_CODE, SUBSYSTEM_CODE, key, null, new Date());
+            }
+
+            // 5. 记录心跳日志(可选,用于调试)
+            log.debug("电力监控系统心跳检测完成 - IP: {}, 状态: {}", ipAddress, isOnline ? "在线" : "离线");
+        }
+        catch (Exception e) {
+            log.error("电力监控系统心跳检测异常", e);
+        }
+    }
+
+    public void execGetDeviceList() {
+        try {
+            // 删除数据库设备
+            QueryDevice param = new QueryDevice();
+            param.setSubsystemCode(SUBSYSTEM_CODE);
+            List<EmsDevice> dbDevs = deviceService.selectList(param);
+
+            if (CollectionUtils.isNotEmpty(dbDevs)) {
+                for (EmsDevice emsDevice : dbDevs) {
+                    deviceService.deleteDevice(emsDevice.getDeviceModel(), emsDevice.getDeviceCode());
+                }
+            }
+
+            objAttrValueService.deleteByObjCode(MONITOR_BHZZ_MODE_CODE, null);
+            objAttrValueService.deleteByObjCode(MONITOR_DB_MODE_CODE, null);
+
+            // 执行采集
+            String[] subIds = StringUtils.split(config.getSubIds(), ",");
+
+            if (ArrayUtils.isNotEmpty(subIds)) {
+                for (String subId : subIds) {
+                    getAreaDeviceList(subId);
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("execGetDeviceList fail!", e);
+        }
+    }
+
+    public void execSyncDevAttrAll() {
+        try {
+            // 执行采集
+            String[] subIds = StringUtils.split(config.getSubIds(), ",");
+            // 采集同步设备
+            Acrel3000Template template = new Acrel3000Template(config.getUrl());
+
+            for (String subId : subIds) {
+                List<MeterUse> meterUseList = template.getMeterUseInfoList(subId);
+                meterUseList = meterUseList.stream().filter(this::meterUseList).collect(Collectors.toList());
+                List<Circuit> circuitList = template.getCircuitInfoTree(subId);
+                Map<String, Circuit> circuitParentMap = circuitList.stream()
+                    .collect(Collectors.toMap(Circuit::getMeterCode, Function.identity()));
+
+                if (CollectionUtils.isNotEmpty(meterUseList)) {
+                    for (MeterUse meterUse : meterUseList) {
+                        String objCode = circuitParentMap.containsKey(meterUse.getMeterCode()) ?
+                            String.format("W4-E-%s", meterUse.getMeterCode()) :
+                            meterUse.getMeterCode();
+
+                        if (StringUtils.equals("01001", meterUse.getMeterTypeCode())) {
+                            refreshDevAttr(objCode, MONITOR_DB_MODE_CODE, meterUse, template);
+                        }
+                        else if (StringUtils.equals("01002", meterUse.getMeterTypeCode())) {
+                            refreshDevAttr(objCode, MONITOR_BHZZ_MODE_CODE, meterUse, template);
+                        }
+                    }
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("execSyncDevAttrAll fail!", e);
+        }
+    }
+
+    public void execSyncDevAttr(String objCode, String modelCode) {
+        try {
+            EmsObjAttrValue subIdObj = objAttrValueService.selectObjAttrValue(modelCode, objCode, "subId");
+            EmsObjAttrValue meterCodeObj = objAttrValueService.selectObjAttrValue(modelCode, objCode, "deviceCode");
+
+            if (null != subIdObj && null != meterCodeObj) {
+                MeterUse meterUse = new MeterUse();
+                meterUse.setSubId(subIdObj.getAttrValue());
+                meterUse.setMeterCode(meterCodeObj.getAttrValue());
+
+                Acrel3000Template template = new Acrel3000Template(config.getUrl());
+                refreshDevAttr(objCode, modelCode, meterUse, template);
+            }
+        }
+        catch (Exception e) {
+            log.error("execSyncDevAttr fail!", e);
+        }
+    }
+
+    public void getAreaDeviceList(String subId) {
+        // 采集同步设备
+        Acrel3000Template template = new Acrel3000Template(config.getUrl());
+        List<MeterUse> meterUseList = template.getMeterUseInfoList(subId);
+        meterUseList = meterUseList.stream().filter(this::meterUseList).collect(Collectors.toList());
+
+        if (CollectionUtils.isNotEmpty(meterUseList)) {
+            Map<String, MeterUse> meterUseMap = meterUseList.stream()
+                .collect(Collectors.toMap(MeterUse::getMeterCode, Function.identity()));
+
+            List<Circuit> circuitList = template.getCircuitInfoTree(subId);
+
+            for (Circuit circuit : circuitList) {
+                if (meterUseMap.containsKey(circuit.getMeterCode())) {
+                    MeterUse meterUse = meterUseMap.get(circuit.getMeterCode());
+
+                    // 构造设备对象
+                    EmsDevice device = convertDevice(meterUse);
+                    deviceService.insertEmsDevice(device);
+
+                    // 设备属性
+                    List<EmsObjAttrValue> attrValues = buildDeviceAttr(device.getDeviceCode(), device.getDeviceModel(),
+                        circuit, meterUseMap, template);
+
+                    if (CollectionUtils.isNotEmpty(attrValues)) {
+                        objAttrValueService.insertBatch(attrValues);
+                    }
+                }
+            }
+        }
+    }
+
+    private void refreshDevAttr(String objCode, String modelCode, MeterUse meterUse, Acrel3000Template template) {
+        // 查询数据库对象属性集合
+        List<EmsObjAttrValue> dbAttrValues = objAttrValueService.selectByObjCode(modelCode, objCode);
+
+        if (CollectionUtils.isNotEmpty(dbAttrValues)) {
+            Map<String, EmsObjAttrValue> vMap = dbAttrValues.stream()
+                .collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, Function.identity()));
+
+            checkAndUpdate(vMap, objCode, modelCode, "deviceStatus", String.valueOf(convertStatus(meterUse)));
+
+            List<EmsObjAttr> objAttrList = objAttrService.selectByModelCode(modelCode);
+            Set<String> attrKeys = objAttrList.stream().map(EmsObjAttr::getAttrKey).collect(Collectors.toSet());
+
+            List<MeterKeyValue> meterKeyValueList = template.getMeterParamValueByKey(meterUse.getSubId(),
+                meterUse.getMeterCode());
+            meterKeyValueFormat(meterKeyValueList, "Switch");
+
+            for (MeterKeyValue meterKeyValue : meterKeyValueList) {
+                if (attrKeys.contains(meterKeyValue.getParamCode())) {
+                    checkAndUpdate(vMap, objCode, modelCode, meterKeyValue.getParamCode(), meterKeyValue.getValue());
+                }
+            }
+        }
+    }
+
+    private List<EmsObjAttrValue> buildDeviceAttr(String objCode, String modelCode, Circuit circuit,
+        Map<String, MeterUse> meterUseMap, Acrel3000Template template) {
+        // 主节点设备自身属性获取
+        MeterUse parentMeterUse = meterUseMap.get(circuit.getMeterCode());
+        List<EmsObjAttrValue> attrValues = new ArrayList<>(
+            getDeviceAttrFromSys(objCode, modelCode, convertStatus(parentMeterUse), circuit, template));
+
+        if (circuit.getInComIng() && CollectionUtils.isNotEmpty(circuit.getChildren())) {
+            JSONArray subDevArray = new JSONArray();
+
+            for (Circuit subCircuit : circuit.getChildren()) {
+                JSONObject jsonObject = new JSONObject();
+                jsonObject.put("deviceCode", subCircuit.getMeterCode());
+                jsonObject.put("modelCode", modelCode);
+                subDevArray.add(jsonObject);
+
+                MeterUse subMeterUse = meterUseMap.get(subCircuit.getMeterCode());
+                // 子节点设备属性获取
+                attrValues.addAll(
+                    getDeviceAttrFromSys(subCircuit.getMeterCode(), modelCode, convertStatus(subMeterUse), subCircuit,
+                        template));
+            }
+
+            attrValues.add(new EmsObjAttrValue(objCode, modelCode, "subDev", subDevArray.toString()));
+        }
+
+        return attrValues;
+    }
+
+    private List<EmsObjAttrValue> getDeviceAttrFromSys(String objCode, String modelCode, Integer deviceStatus,
+        Circuit circuit, Acrel3000Template template) {
+        List<EmsObjAttrValue> retList = new ArrayList<>();
+        retList.add(new EmsObjAttrValue(objCode, modelCode, "subId", circuit.getSubId()));
+        retList.add(new EmsObjAttrValue(objCode, modelCode, "subName", circuit.getSubName()));
+        retList.add(new EmsObjAttrValue(objCode, modelCode, "deviceName", circuit.getCircuitName()));
+        retList.add(new EmsObjAttrValue(objCode, modelCode, "deviceCode", circuit.getMeterCode()));
+        retList.add(new EmsObjAttrValue(objCode, modelCode, "deviceStatus", deviceStatus.toString()));
+
+        List<MeterKeyValue> meterKeyValueList = template.getMeterParamValueByKey(circuit.getSubId(),
+            circuit.getMeterCode());
+
+        if (CollectionUtils.isNotEmpty(meterKeyValueList)) {
+            List<EmsObjAttr> objAttrList = objAttrService.selectByModelCode(modelCode);
+
+            Map<String, MeterKeyValue> map = meterKeyValueList.stream()
+                .collect(Collectors.toMap(MeterKeyValue::getParamCode, Function.identity()));
+            // 值格式化
+            meterKeyValueFormat(map, "Switch");
+
+            for (EmsObjAttr objAttr : objAttrList) {
+                if (map.containsKey(objAttr.getAttrKey())) {
+                    MeterKeyValue meterKeyValue = map.get(objAttr.getAttrKey());
+                    retList.add(
+                        new EmsObjAttrValue(objCode, modelCode, objAttr.getAttrKey(), meterKeyValue.getValue()));
+                }
+            }
+        }
+
+        return retList;
+    }
+
+    private void meterKeyValueFormat(List<MeterKeyValue> list, String key) {
+        Map<String, MeterKeyValue> map = list.stream()
+            .collect(Collectors.toMap(MeterKeyValue::getParamCode, Function.identity()));
+        meterKeyValueFormat(map, key);
+    }
+
+    private void meterKeyValueFormat(Map<String, MeterKeyValue> map, String key) {
+        MeterKeyValue switchMeterKeyValue = map.get(key);
+
+        if (null != switchMeterKeyValue && null != switchMeterKeyValue.getValue()) {
+            switchMeterKeyValue.setValue(switchMeterKeyValue.getValue().split("\\.")[0]);
+        }
+    }
+
+    private EmsDevice convertDevice(MeterUse meterUse) {
+        EmsDevice device = new EmsDevice();
+        device.setDeviceCode(String.format("W4-E-%s", meterUse.getMeterCode()));
+        device.setDeviceName(meterUse.getMeterName());
+        device.setDeviceBrand("安科瑞");
+        device.setDeviceSpec(null);
+        device.setDeviceStatus(convertStatus(meterUse));
+        device.setLocation(meterUse.getSubName());
+        device.setPsCode("SM");
+        device.setSubsystemCode(SUBSYSTEM_CODE);
+
+        if (StringUtils.equals("01001", meterUse.getMeterTypeCode())) {
+            device.setDeviceModel(MONITOR_DB_MODE_CODE);
+        }
+        else if (StringUtils.equals("01002", meterUse.getMeterTypeCode())) {
+            device.setDeviceModel(MONITOR_BHZZ_MODE_CODE);
+        }
+
+        if (StringUtils.equals("10100002", meterUse.getSubId())) {
+            device.setLocationRef("321283124S300100");
+            device.setAreaCode("321283124S3001");
+            device.setRefFacs("NH01");
+        }
+        else if (StringUtils.equals("10100003", meterUse.getSubId())) {
+            device.setLocationRef("321283124S300200");
+            device.setAreaCode("321283124S3002");
+            device.setRefFacs("NH02");
+        }
+
+        return device;
+    }
+
+    private Integer convertStatus(MeterUse meterUse) {
+        if (null != meterUse && null != meterUse.getDisConnect()) {
+            return meterUse.getDisConnect() ? 0 : 1;
+        }
+        else {
+            return 1;
+        }
+    }
+
+    private boolean meterUseList(MeterUse meterUse) {
+        return
+            (StringUtils.equals(meterUse.getMeterTypeCode(), "01001") && !StringUtils.contains(meterUse.getMeterName(),
+                "温感")) || StringUtils.equals(meterUse.getMeterTypeCode(), "01002");
+    }
+}

+ 170 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/AlarmEventLog.java

@@ -0,0 +1,170 @@
+/*
+ * 文 件 名:  AlarmEventLog
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/25
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.acrel.model;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * 告警事件日志
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/25]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class AlarmEventLog {
+    /**
+     * 事件id
+     */
+    @JSONField(name = "fAlarmeventlogid")
+    private String alarmEventLogId;
+
+    /**
+     * 站点id
+     */
+    @JSONField(name = "fSubid")
+    private Integer subId;
+
+    /**
+     * 站点名称
+     */
+    @JSONField(name = "fSubname")
+    private String subName;
+
+    /**
+     * 回路id
+     */
+    @JSONField(name = "fCircuitid")
+    private String circuitId;
+
+    /**
+     * fType
+     */
+    @JSONField(name = "fType")
+    private Integer fType;
+
+    /**
+     * 报警类型编码
+     */
+    @JSONField(name = "fMessinfocode")
+    private String messInfoCode;
+
+    /**
+     * 报警类型  2:通讯状态, 3:现场报警 4:设备故障 33:一级报警 35:二级报警 36:三级报警
+     */
+    @JSONField(name = "fMessinfotypeid")
+    private Integer messInfoTypeId;
+
+    /**
+     * 事件类型
+     */
+    @JSONField(name = "fMessInfoExplain")
+    private String messInfoExplain;
+
+    /**
+     * 报警类型详情
+     */
+    @JSONField(name = "fMessInfoTypeExplain")
+    private String messInfoTypeExplain;
+
+    /**
+     * 报警等级详情
+     */
+    @JSONField(name = "fMessinfolevelexplain")
+    private String messInfoLevelExplain;
+
+    /**
+     * 报警等级
+     */
+    @JSONField(name = "fMessinfolevel")
+    private Integer messInfoLevel;
+
+    /**
+     * 设备编号
+     */
+    @JSONField(name = "fDevicecode")
+    private String deviceCode;
+
+    /**
+     * 设备名称
+     */
+    @JSONField(name = "fDevicename")
+    private String deviceName;
+
+    /**
+     * 参数编码(仪表报警有,网关没有)
+     */
+    @JSONField(name = "fParamcode")
+    private String paramCode;
+
+    /**
+     * 参数名称
+     */
+    @JSONField(name = "fParamname")
+    private String paramName;
+
+    /**
+     * 参数对应值说明
+     */
+    @JSONField(name = "fValueType")
+    private String valueType;
+
+    /**
+     * 参数对应值(仪表报警有,网关没有)
+     */
+    @JSONField(name = "fValue")
+    private String value;
+
+    /**
+     * 限制值
+     */
+    @JSONField(name = "fLimitvalue")
+    private String limitValue;
+
+    /**
+     * 报警类型
+     */
+    @JSONField(name = "fAlarmtype")
+    private String alarmType;
+
+    /**
+     * 报警详情
+     */
+    @JSONField(name = "fAlarmdesc")
+    private String alarmDesc;
+
+    /**
+     * 报警时间
+     */
+    @JSONField(name = "fAlarmtime")
+    private String alarmTime;
+
+    /**
+     * 确认状态
+     */
+    @JSONField(name = "fConfirmstatus")
+    private Boolean confirmStatus;
+
+    @JSONField(name = "fCoaccountno")
+    private String coacCountNo;
+
+    @JSONField(name = "fConame")
+    private String coName;
+
+    @JSONField(name = "fContact")
+    private String contacts;
+
+    @JSONField(name = "fContactsPhone")
+    private String contactsPhone;
+}

+ 103 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/Circuit.java

@@ -0,0 +1,103 @@
+/*
+ * 文 件 名:  Circuit
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/21
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.acrel.model;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 回路信息
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/21]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class Circuit {
+    /**
+     * 站点id
+     */
+    @JSONField(name = "fSubid")
+    private String subId;
+
+    /**
+     * 站点名称
+     */
+    @JSONField(name = "fSubname")
+    private String subName;
+
+    /**
+     * 站点名称-英文
+     */
+    @JSONField(name = "fSubnameEn")
+    private String subNameEn;
+
+    /**
+     * 回路id
+     */
+    @JSONField(name = "fCircuitid")
+    private String circuitId;
+
+    /**
+     * 回路名称
+     */
+    @JSONField(name = "fCircuitname")
+    private String circuitName;
+
+    /**
+     * 父级回路id
+     */
+    @JSONField(name = "fParentid")
+    private String parentId;
+
+    /**
+     * 状态
+     */
+    @JSONField(name = "fState")
+    private Integer state;
+
+    /**
+     * 是否进线
+     */
+    @JSONField(name = "fIsincoming")
+    private Boolean inComIng;
+
+    /**
+     * 仪表编号
+     */
+    @JSONField(name = "fMetercode")
+    private String meterCode;
+
+    /**
+     * 排序号
+     */
+    @JSONField(name = "fSortNum")
+    private Integer sortNum;
+
+    /**
+     * 开关状态
+     */
+    @JSONField(name = "fSwitchstatus")
+    private Boolean switchStatus;
+
+    /**
+     * 告警
+     */
+    @JSONField(name = "isAlarm")
+    private Integer alarm;
+
+    @JSONField(name = "children")
+    private List<Circuit> children;
+}

+ 31 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/ConsumeReport.java

@@ -0,0 +1,31 @@
+/*
+ * 文 件 名:  ConsumeReport
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/23
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.acrel.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 自定义集抄对象
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/23]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class ConsumeReport {
+    private List<String> datetimeList;
+
+    private List<ReportNode> energyReport;
+}

+ 38 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/EnergyValue.java

@@ -0,0 +1,38 @@
+/*
+ * 文 件 名:  EnergyValue
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/23
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.acrel.model;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * 抄报读数
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/23]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class EnergyValue {
+    @JSONField(name = "fCollecttime")
+    private String collecttime;
+
+    @JSONField(name = "fOrigValue")
+    private String origValue;
+
+    @JSONField(name = "fConsumeValue")
+    private Double consumeValue;
+
+    @JSONField(name = "fConsumeFee")
+    private Double consumeFee;
+}

+ 68 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/MeterKeyValue.java

@@ -0,0 +1,68 @@
+/*
+ * 文 件 名:  MeterKeyValue
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/23
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.acrel.model;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * 计量返回键值对
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/23]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class MeterKeyValue {
+    /**
+     * 站点ID
+     */
+    @JSONField(name = "fSubid")
+    private Integer subId;
+
+    /**
+     * 仪表编码
+     */
+    @JSONField(name = "fMetercode")
+    private String meterCode;
+
+    /**
+     * 仪表名称
+     */
+    @JSONField(name = "fMetername")
+    private String meterName;
+
+    /**
+     * 参数
+     */
+    @JSONField(name = "fParamcode")
+    private String paramCode;
+
+    /**
+     * 参数名称
+     */
+    @JSONField(name = "fParamname")
+    private String paramName;
+
+    /**
+     * 值
+     */
+    @JSONField(name = "fValue")
+    private String value;
+
+    /**
+     * 值单位
+     */
+    @JSONField(name = "fUnitCode")
+    private String unitCode;
+}

+ 136 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/MeterUse.java

@@ -0,0 +1,136 @@
+/*
+ * 文 件 名:  MeterUse
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/21
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.acrel.model;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * 计量装置
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/21]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class MeterUse {
+    /**
+     * 仪表编号
+     */
+    @JSONField(name = "fMetercode")
+    private String meterCode;
+
+    /**
+     * 站点id
+     */
+    @JSONField(name = "fSubid")
+    private String subId;
+
+    /**
+     * 站点英文名称
+     */
+    @JSONField(name = "fSubnameEN")
+    private String subNameEn;
+
+    /**
+     * 站点名称
+     */
+    @JSONField(name = "fSubname")
+    private String subName;
+
+    /**
+     * 网关id
+     */
+    @JSONField(name = "fGatewayid")
+    private String gatewayId;
+
+    /**
+     * 仪表名称
+     */
+    @JSONField(name = "fMetername")
+    private String meterName;
+
+    /**
+     * 串口号
+     */
+    @JSONField(name = "fSerialport")
+    private String serialPort;
+
+    /**
+     * 仪表地址
+     */
+    @JSONField(name = "fMeteraddr")
+    private String meterAddr;
+
+    /**
+     * 是否离线
+     */
+    @JSONField(name = "fIsdisconnnect")
+    private Boolean disConnect;
+
+    /**
+     * 仪表类型编号
+     */
+    @JSONField(name = "fMetertypecode")
+    private String meterTypeCode;
+
+    /**
+     * 仪表状态编号
+     * </br>1:使用 0:不使用
+     */
+    @JSONField(name = "fState")
+    private Integer state;
+
+    /**
+     * 仪表状态
+     */
+    @JSONField(name = "fStateExplain")
+    private String stateExplain;
+
+    /**
+     * 能源类型
+     * 0:电 1:水 2:气
+     */
+    @JSONField(name = "fEnergyType")
+    private Integer energyType;
+
+    /**
+     * 仪表类型名称
+     */
+    @JSONField(name = "fMetertypename")
+    private String fMetertypename;
+
+    /**
+     * 第三方仪表是否存在
+     */
+    @JSONField(name = "thirdpartyState")
+    private Integer thirdPartyState;
+
+    /**
+     * IOT设备是否存在
+     */
+    @JSONField(name = "outfireState")
+    private Integer outFireState;
+
+    /**
+     * 是否虚拟表
+     */
+    @JSONField(name = "fIsreal")
+    private Boolean real;
+
+    /**
+     * 仪表型号
+     */
+    @JSONField(name = "fModelName")
+    private String fModelName;
+}

+ 40 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/ReportNode.java

@@ -0,0 +1,40 @@
+/*
+ * 文 件 名:  ReportNode
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/23
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.acrel.model;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 抄报对象
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/23]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class ReportNode {
+    @JSONField(name = "fCircuitid")
+    private String circuitId;
+
+    @JSONField(name = "fCircuitname")
+    private String circuitName;
+
+    @JSONField(name = "fIsincoming")
+    private Boolean fIsincoming;
+
+    @JSONField(name = "origEnergyValues")
+    private List<EnergyValue> origEnergyValues;
+}

+ 424 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/Station.java

@@ -0,0 +1,424 @@
+/*
+ * 文 件 名:  Station
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/21
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.acrel.model;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 站点信息
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/21]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class Station {
+    /**
+     * 站点编号
+     */
+    @JSONField(name = "fSubid")
+    private Long subId;
+
+    /**
+     * 站点区域分组编号
+     */
+    @JSONField(name = "fSubgroupid")
+    private Long subgroupId;
+
+    /**
+     * 站点区域分组名称
+     */
+    @JSONField(name = "fSubgroupName")
+    private String subGroupName;
+
+    /**
+     * 站点区域分组名称英文
+     */
+    @JSONField(name = "fSubgroupNameen")
+    private String subGroupNameEn;
+
+    /**
+     * 父站点编号
+     */
+    @JSONField(name = "fParentid")
+    private Long parentId;
+
+    /**
+     * 父级站点名称
+     */
+    @JSONField(name = "fParentname")
+    private String parentName;
+
+    /**
+     * 父级站点英文名称
+     */
+    @JSONField(name = "fParentnameen")
+    private String parentNameEn;
+
+    /**
+     * 站点名称
+     */
+    @JSONField(name = "fSubname")
+    private String subName;
+
+    /**
+     * 站点简称
+     */
+    @JSONField(name = "fSubshort")
+    private String subShort;
+
+    /**
+     * 站点图片地址
+     */
+    @JSONField(name = "substationImg")
+    private String substationImg;
+
+    /**
+     * 3d的url地址
+     */
+    @JSONField(name = "f3durl")
+    private String url3d;
+
+    /**
+     * 站点地址
+     */
+    @JSONField(name = "fAddress")
+    private String address;
+
+    /**
+     * 经度
+     */
+    @JSONField(name = "fLongitude")
+    private Double longitude;
+
+    /**
+     * 纬度
+     */
+    @JSONField(name = "fLatitude")
+    private Double latitude;
+
+    /**
+     * 企业编号
+     */
+    @JSONField(name = "fCoaccountno")
+    private String coAccountNo;
+
+    /**
+     * 企业名称
+     */
+    @JSONField(name = "fConame")
+    private String coName;
+
+    /**
+     * 企业名称英文
+     */
+    @JSONField(name = "fConameen")
+    private String coNameEn;
+
+    /**
+     * 价格类型
+     * 1:单电价,2:尖峰平谷电价
+     */
+    @JSONField(name = "priceType")
+    private String priceType;
+
+    /**
+     * 折扣
+     */
+    @JSONField(name = "rate")
+    private String rate;
+
+    /**
+     * 平时间段
+     */
+    @JSONField(name = "fEpiptime")
+    private String epiPTime;
+
+    /**
+     * 峰时间段
+     */
+    @JSONField(name = "fEpiftime")
+    private String epiFTime;
+
+    /**
+     * 谷时间段
+     */
+    @JSONField(name = "fEpigtime")
+    private String epiGTime;
+
+    /**
+     * 尖时间段
+     */
+    @JSONField(name = "fEpigtime")
+    private String epiJTime;
+
+    /**
+     * 平时段电价
+     */
+    @JSONField(name = "f_EpiPPrice")
+    private String epiPPrice;
+
+    /**
+     * 峰时段电价
+     */
+    @JSONField(name = "f_EpiFPrice")
+    private String epiFPrice;
+
+    /**
+     * 谷时段电价
+     */
+    @JSONField(name = "f_EpiGPrice")
+    private String epiGPrice;
+
+    /**
+     * 尖时段电价
+     */
+    @JSONField(name = "f_EpiJPrice")
+    private String epiJPrice;
+
+    /**
+     * 单一电价
+     */
+    @JSONField(name = "f_EpiSinglePrice")
+    private String epiSinglePrice;
+
+    /**
+     * 电价-5
+     */
+    @JSONField(name = "f_Epi5Price")
+    private String epi5Price;
+
+    /**
+     * 电价-6
+     */
+    @JSONField(name = "f_Epi6Price")
+    private String epi6Price;
+
+    /**
+     * 电价-8
+     */
+    @JSONField(name = "f_Epi8Price")
+    private String epi8Price;
+
+    /**
+     * 装机容量
+     */
+    @JSONField(name = "installEdcapacity")
+    private Integer installEdcApacity;
+
+    /**
+     * 电压等级
+     */
+    @JSONField(name = "fVoltagestep")
+    private Double voltageStep;
+
+    /**
+     * 变电电压
+     */
+    @JSONField(name = "fVoltageoftrans")
+    private String voltageOfTrans;
+
+    /**
+     * 站点类型
+     * <br/>0:变配电站,1:光伏电站,2:供水站,3:供气站,4:供冷站,5:供热站,6:储能站,7:风电站,8:瓦斯发电站
+     */
+    @JSONField(name = "fType")
+    private Integer fType;
+
+    /**
+     * 巡检配置
+     * <br/>2:启用自定义,3:启用停用配置
+     */
+    @JSONField(name = "fConfigtypeid")
+    private Integer configTypeId;
+
+    /**
+     * 视界直播
+     */
+    @JSONField(name = "fVisionlive")
+    private String visionLive;
+
+    /**
+     * 是否计量标志
+     */
+    @JSONField(name = "fIscount")
+    private Boolean count;
+
+    /**
+     * 变压器数量
+     */
+    @JSONField(name = "fTransformernum")
+    private Integer transformerNum;
+
+    /**
+     * 门禁
+     */
+    @JSONField(name = "fDoor")
+    private Boolean door;
+
+    /**
+     * 申报容量
+     */
+    @JSONField(name = "fApplycapacity")
+    private Integer applyCapacity;
+
+    /**
+     * 储能装机容量
+     */
+    @JSONField(name = "fStorageInstalledCapacity")
+    private String storageInstalledCapacity;
+
+    /**
+     * 电池类型
+     * <br/>0:锂电池,1:液流电池
+     */
+    @JSONField(name = "fBatteryType")
+    private String batteryType;
+
+    /**
+     * 补贴截止日期
+     */
+    @JSONField(name = "fSubsidyendtime")
+    private String subsidyEndTime;
+
+    /**
+     * 补贴电价
+     */
+    @JSONField(name = "fSubsidyprice")
+    private String subsidyPrice;
+
+    /**
+     * 二氧化碳
+     */
+    @JSONField(name = "fCo2")
+    private String co2;
+
+    /**
+     * 标准煤
+     */
+    @JSONField(name = "fFml")
+    private String ml;
+
+    /**
+     * STS开关检测功能
+     * <br/>0:关,1:开
+     */
+    @JSONField(name = "fSTS")
+    private Integer sts;
+
+    /**
+     * 报警手机号
+     */
+    @JSONField(name = "fPhoneofalarm")
+    private String phoneOfAlarm;
+
+    /**
+     * 联系人
+     */
+    @JSONField(name = "fContacts")
+    private String contacts;
+
+    /**
+     * 联系人电话
+     */
+    @JSONField(name = "fContactsPhone")
+    private String contactsPhone;
+
+    /**
+     * 默认负责人
+     */
+    @JSONField(name = "fDefaultcharger")
+    private String defaultCharger;
+
+    /**
+     * 默认执行人
+     */
+    @JSONField(name = "fDefaultexecutor")
+    private String defaultExecutor;
+
+    /**
+     * 运行状态手机号
+     */
+    @JSONField(name = "fPhoneofoperation")
+    private String phoneOfOperation;
+
+    /**
+     * 第三方用户平台
+     */
+    @JSONField(name = "f_PartnerSubNo")
+    private String partnerSubNo;
+
+    /**
+     * 水浸
+     */
+    @JSONField(name = "fWaterin")
+    private Boolean waterIn;
+
+    /**
+     * 烟雾
+     */
+    @JSONField(name = "fSmog")
+    private Boolean smog;
+
+    /**
+     * 温湿度表
+     */
+    @JSONField(name = "fTHMeter")
+    private String thMeter;
+
+    /**
+     * 投运时间
+     */
+    @JSONField(name = "fOperatetime")
+    private Long operateTime;
+
+    /**
+     * 光伏站点计算类型
+     * <br/>1:纯并网,2:余电上网
+     */
+    @JSONField(name = "energyType")
+    private String energyType;
+
+    /**
+     * 建造商
+     */
+    @JSONField(name = "fBuilder")
+    private String builder;
+
+    /**
+     * 并网电压
+     */
+    @JSONField(name = "fGridvoltage")
+    private String fGridvoltage;
+
+    /**
+     * 站点类型信息
+     */
+    @JSONField(name = "type")
+    private StationType type;
+
+    /**
+     * 时段电价信息
+     */
+    @JSONField(name = "fEpitimelist")
+    private List<StationEpiTime> epiTimeList;
+
+    /**
+     * 子站点信息
+     */
+    @JSONField(name = "children")
+    private List<Station> children;
+}

+ 35 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/StationEpiTime.java

@@ -0,0 +1,35 @@
+/*
+ * 文 件 名:  StationEpitime
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/21
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.acrel.model;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * 时段电价
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/21]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class StationEpiTime {
+    @JSONField(name = "fSubid")
+    private Integer fSubId;
+
+    @JSONField(name = "fType")
+    private Integer fType;
+
+    @JSONField(name = "fEpitime")
+    private String fEpiTime;
+}

+ 45 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/StationType.java

@@ -0,0 +1,45 @@
+/*
+ * 文 件 名:  StationType
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/21
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.acrel.model;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * 站点类型信息
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/21]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class StationType {
+    /**
+     * 站点类型英文名称
+     */
+    @JSONField(name = "fStationtypenameen")
+    private String fStationTypeNameEn;
+
+    /**
+     * 站点类型名称
+     */
+    @JSONField(name = "fStationtypename")
+    private String fStationTypeName;
+
+    /**
+     * 站点类型编码
+     * <br/>0:变配电站,1:光伏电站,2:供水站,3:供气站,4:供冷站,5:供热站,6:储能站,7:风电站,8:瓦斯发电站
+     */
+    @JSONField(name = "fStationtypecode")
+    private String fStationTypeCode;
+}

+ 35 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/model/Token.java

@@ -0,0 +1,35 @@
+/*
+ * 文 件 名:  TokenRes
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/21
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.acrel.model;
+
+import lombok.Data;
+
+/**
+ * Token对象
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/21]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class Token {
+    /**
+     * 过期时间
+     */
+    private String expireDate;
+
+    /**
+     * token
+     */
+    private String token;
+}

+ 1 - 1
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/retrofit/Acrel3000Api.java → ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/retrofit/Acrel3000Api.java

@@ -8,7 +8,7 @@
  * 修改单号:  <修改单号>
  * 修改内容:  <修改内容>
  */
-package com.ruoyi.ems.retrofit;
+package com.ruoyi.ems.acrel.retrofit;
 
 import okhttp3.RequestBody;
 import retrofit2.Call;

+ 399 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/acrel/retrofit/Acrel3000Template.java

@@ -0,0 +1,399 @@
+/*
+ * 文 件 名:  Acrel3000Template
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/19
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.acrel.retrofit;
+
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.support.retrofit.Retrofit2ConverterFactory;
+import com.huashe.common.exception.Assert;
+import com.huashe.common.utils.DateUtils;
+import com.ruoyi.common.core.utils.SpringUtils;
+import com.ruoyi.common.redis.service.RedisService;
+import com.ruoyi.ems.acrel.config.AcrelConfig;
+import com.ruoyi.ems.acrel.model.AlarmEventLog;
+import com.ruoyi.ems.acrel.model.Circuit;
+import com.ruoyi.ems.acrel.model.ConsumeReport;
+import com.ruoyi.ems.acrel.model.MeterKeyValue;
+import com.ruoyi.ems.acrel.model.MeterUse;
+import com.ruoyi.ems.acrel.model.Station;
+import com.ruoyi.ems.acrel.model.Token;
+import com.ruoyi.ems.common.retrofit.BaseApiTemplate;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.RequestBody;
+import org.apache.commons.lang3.StringUtils;
+import retrofit2.Call;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 安科瑞3000系列模板类
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/19]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Slf4j
+public class Acrel3000Template extends BaseApiTemplate {
+    /**
+     * 访问地址
+     */
+    protected String url;
+
+    /**
+     * 调用代理
+     */
+    protected final Acrel3000Api api;
+
+    public Acrel3000Template(String restUrl) {
+        this.url = restUrl;
+
+        OkHttpClient httpClient = getClient();
+        Retrofit retrofit = new Retrofit.Builder().baseUrl(restUrl)
+            .addConverterFactory(Retrofit2ConverterFactory.create()).client(httpClient).build();
+        this.api = retrofit.create(Acrel3000Api.class);
+    }
+
+    /**
+     * 构造调用模板
+     *
+     * @param restUrl        服务地址 (http://ip:port)
+     * @param connectTimeout 连接超时
+     * @param readTimeout    读取超时
+     * @param writeTimeout   写超时
+     */
+    public Acrel3000Template(String restUrl, int connectTimeout, int readTimeout, int writeTimeout) {
+        super.connectTimeout = connectTimeout;
+        super.readTimeout = readTimeout;
+        super.writeTimeout = writeTimeout;
+        this.url = restUrl;
+
+        OkHttpClient httpClient = getClient();
+        Retrofit retrofit = new Retrofit.Builder().baseUrl(restUrl)
+            .addConverterFactory(Retrofit2ConverterFactory.create()).client(httpClient).build();
+        api = retrofit.create(Acrel3000Api.class);
+    }
+
+    public synchronized String getAuthToken() {
+        RedisService redisService = SpringUtils.getBean(RedisService.class);
+        String tokenStr = redisService.getCacheObject("ACREL_AUTH_TOKEN");
+
+        if (StringUtils.isEmpty(tokenStr)) {
+            AcrelConfig config = SpringUtils.getBean(AcrelConfig.class);
+            Token token = getAuthToken(config.getAuthLoginName(), config.getAuthPassword());
+            Assert.notNull(token, -1, "获取权限失败");
+
+            Date expireDate = DateUtils.stringToDate(token.getExpireDate(), "yyyy-MM-dd HH:mm:ss");
+            Date currentDate = new Date();
+            long expireSeconds = (expireDate.getTime() - currentDate.getTime()) / 1000;
+            Assert.isTrue(expireSeconds > 0, -1, "Token已过期或过期时间无效");
+            redisService.setCacheObject("ACREL_AUTH_TOKEN", token.getToken(), expireSeconds, TimeUnit.SECONDS);
+            tokenStr = token.getToken();
+        }
+
+        return tokenStr;
+    }
+
+    /**
+     * 获取权限
+     *
+     * @param loginName 用户名
+     * @param password  密码
+     * @return 响应
+     */
+    public Token getAuthToken(String loginName, String password) {
+        Token token = null;
+
+        try {
+            // 创建请求体
+            RequestBody loginnameBody = RequestBody.create(MediaType.parse("text/plain"), loginName);
+            RequestBody passwordBody = RequestBody.create(MediaType.parse("text/plain"), password);
+
+            // 执行调用
+            Call<String> call = api.getAuthToken(loginnameBody, passwordBody);
+            Response<String> response = call.execute();
+            log.debug("getAuthToken response:{}", response);
+
+            JSONObject resJson = JSONObject.parseObject(response.body());
+            Assert.isTrue(StringUtils.equals(resJson.getString("code"), "200"), resJson.getInteger("code"),
+                resJson.getString("msg"));
+
+            String dataStr = resJson.getString("data");
+            token = JSONObject.parseObject(dataStr, Token.class);
+        }
+        catch (Exception e) {
+            log.error("getAuthToken fail!", e);
+        }
+
+        return token;
+    }
+
+    /**
+     * 获取站点列表
+     *
+     * @param coacCountNo 企业编号(和站点区域分组id不能同时存在)
+     * @param subGroupId  站点区域分组编号(和企业编号不能同时存在)
+     * @return 响应
+     */
+    public List<Station> getStationTreeList(String coacCountNo, String subGroupId) {
+        List<Station> stations = null;
+
+        try {
+            // 动态构建参数字段
+            Map<String, RequestBody> fields = new HashMap<>();
+            fields.put("type", RequestBody.create(MediaType.parse("text/plain"), "0"));
+
+            if (StringUtils.isNotBlank(coacCountNo)) {
+                fields.put("fCoaccountno", RequestBody.create(MediaType.parse("text/plain"), coacCountNo));
+            }
+
+            if (StringUtils.isNotBlank(subGroupId)) {
+                fields.put("fSubgroupid", RequestBody.create(MediaType.parse("text/plain"), subGroupId));
+            }
+
+            // 执行调用
+            Call<String> call = api.getStationTreeList(getAuthToken(), fields);
+            Response<String> response = call.execute();
+            log.debug("getStationTreeList response:{}", response.body());
+
+            JSONObject resJson = JSONObject.parseObject(response.body());
+            Assert.isTrue(StringUtils.equals(resJson.getString("code"), "200"), resJson.getInteger("code"),
+                resJson.getString("msg"));
+
+            JSONObject dataJson = resJson.getJSONObject("data");
+            int num = dataJson.getInteger("num");
+
+            if (num > 0) {
+                String list = dataJson.getString("list");
+                stations = JSONObject.parseArray(list, Station.class);
+            }
+            else {
+                stations = new ArrayList<>();
+            }
+        }
+        catch (Exception e) {
+            log.error("getStationTreeList fail!", e);
+        }
+
+        return stations;
+    }
+
+    /**
+     * 获取仪表列表
+     *
+     * @param subId 站点ID
+     * @return 响应
+     */
+    public List<MeterUse> getMeterUseInfoList(String subId) {
+        List<MeterUse> list = new ArrayList<>();
+
+        try {
+            int pageNo = 1;
+            int pageSize = 20;
+            int english = 0;
+            boolean isLastPage;
+
+            do {
+                // 执行调用
+                Call<String> call = api.getMeterUseInfoList(getAuthToken(), subId, pageNo, pageSize, english);
+                Response<String> response = call.execute();
+                log.debug("getMeterUseInfoList response:{}", response);
+
+                JSONObject resJson = JSONObject.parseObject(response.body());
+                Assert.isTrue(StringUtils.equals(resJson.getString("code"), "200"), resJson.getInteger("code"),
+                    resJson.getString("msg"));
+
+                JSONObject dataJson = resJson.getJSONObject("data");
+                isLastPage = dataJson.getBoolean("isLastPage");
+                String listStr = dataJson.getString("list");
+                list.addAll(JSONObject.parseArray(listStr, MeterUse.class));
+
+                pageNo++;
+            }
+            while (!isLastPage);
+        }
+        catch (Exception e) {
+            log.error("getMeterUseInfoList fail!", e);
+        }
+
+        return list;
+    }
+
+    /**
+     * 获取回路列表
+     *
+     * @param subId 站点ID
+     * @return 响应
+     */
+    public List<Circuit> getCircuitInfoTree(String subId) {
+        List<Circuit> circuitList = null;
+
+        try {
+            // 执行调用
+            Call<String> call = api.getCircuitInfoTree(getAuthToken(), subId);
+            Response<String> response = call.execute();
+            log.debug("getCircuitInfoTree response:{}", response);
+
+            JSONObject resJson = JSONObject.parseObject(response.body());
+            Assert.isTrue(StringUtils.equals(resJson.getString("code"), "200"), resJson.getInteger("code"),
+                resJson.getString("msg"));
+
+            String dataStr = resJson.getString("data");
+            circuitList = JSONObject.parseArray(dataStr, Circuit.class);
+        }
+        catch (Exception e) {
+            log.error("getCircuitInfoTree fail!", e);
+        }
+
+        return circuitList;
+    }
+
+    /**
+     * 获取实时数据
+     *
+     * @param subId     站点ID
+     * @param meterCode 设备ID
+     * @return 响应
+     */
+    public List<MeterKeyValue> getMeterParamValueByKey(String subId, String meterCode) {
+        List<MeterKeyValue> meterKvList = null;
+
+        try {
+            // 执行调用
+            Call<String> call = api.getMeterParamValueByKey(getAuthToken(), subId, meterCode);
+            Response<String> response = call.execute();
+            log.debug("getMeterParamValueByKey response:{}", response);
+
+            JSONObject resJson = JSONObject.parseObject(response.body());
+            Assert.isTrue(StringUtils.equals(resJson.getString("code"), "200"), resJson.getInteger("code"),
+                resJson.getString("msg"));
+
+            JSONObject dataJson = resJson.getJSONObject("data");
+            Assert.notNull(dataJson, -1, "data is null!");
+
+            String resultStr = dataJson.getString("result");
+            meterKvList = JSONObject.parseArray(resultStr, MeterKeyValue.class);
+        }
+        catch (Exception e) {
+            log.error("getMeterParamValueByKey fail!", e);
+        }
+
+        return meterKvList;
+    }
+
+    /**
+     * 用能集抄
+     *
+     * @param circuitIds 回路ID集合
+     * @param type       类型 min:分, hour: 时, day: 日, mon: 月
+     * @return 响应
+     */
+    public ConsumeReport getNewConsumeReport(String circuitIds, String type, String startTime, String endTime,
+        String interval, String paramCode) {
+        ConsumeReport consumeReport = null;
+
+        try {
+            // 动态构建参数字段
+            Map<String, RequestBody> fields = new HashMap<>();
+            fields.put("fCircuitids", RequestBody.create(MediaType.parse("text/plain"), circuitIds));
+            fields.put("type", RequestBody.create(MediaType.parse("text/plain"), type));
+            fields.put("startTime", RequestBody.create(MediaType.parse("text/plain"), startTime));
+            fields.put("endTime", RequestBody.create(MediaType.parse("text/plain"), endTime));
+            fields.put("interval", RequestBody.create(MediaType.parse("text/plain"), interval));
+            fields.put("paramCode", RequestBody.create(MediaType.parse("text/plain"), paramCode));
+
+            // 执行调用
+            Call<String> call = api.getNewConsumeReport(getAuthToken(), fields);
+            Response<String> response = call.execute();
+            log.debug("getNewConsumeReport response:{}", response);
+
+            JSONObject resJson = JSONObject.parseObject(response.body());
+            Assert.isTrue(StringUtils.equals(resJson.getString("code"), "200"), resJson.getInteger("code"),
+                resJson.getString("msg"));
+
+            String dataStr = resJson.getString("data");
+            consumeReport = JSONObject.parseObject(dataStr, ConsumeReport.class);
+        }
+        catch (Exception e) {
+            log.error("getNewConsumeReport fail!", e);
+        }
+
+        return consumeReport;
+    }
+
+    /**
+     * 获取报警事件列表
+     * @param subId 站点ID
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @return 响应
+     */
+    public List<AlarmEventLog> getAlarmEventLogList(String subId, String startTime, String endTime) {
+        List<AlarmEventLog> list = new ArrayList<>();
+
+        try {
+            int pageNo = 1;
+            int pageSize = 20;
+            boolean isLastPage;
+
+            do {
+                // 动态构建参数字段
+                Map<String, RequestBody> fields = new HashMap<>();
+
+                if (null != subId) {
+                    fields.put("subId", RequestBody.create(MediaType.parse("text/plain"), subId));
+                }
+
+                fields.put("startTime", RequestBody.create(MediaType.parse("text/plain"), startTime));
+                fields.put("endTime", RequestBody.create(MediaType.parse("text/plain"), endTime));
+                fields.put("pageNo", RequestBody.create(MediaType.parse("text/plain"), String.valueOf(pageNo)));
+                fields.put("pageSize", RequestBody.create(MediaType.parse("text/plain"), String.valueOf(pageSize)));
+
+                // 执行调用
+                Call<String> call = api.getAlarmEventLogList(getAuthToken(), fields);
+                Response<String> response = call.execute();
+                log.debug("getAlarmEventLogList response:{}", response);
+
+                JSONObject resJson = JSONObject.parseObject(response.body());
+                Assert.isTrue(StringUtils.equals(resJson.getString("code"), "200"), resJson.getInteger("code"),
+                    resJson.getString("msg"));
+
+                JSONObject dataJson = resJson.getJSONObject("data");
+                Assert.notNull(dataJson, -1, "data is null!");
+                JSONObject alarmJson = dataJson.getJSONObject("alarmEventLogList");
+
+                isLastPage = alarmJson.getBoolean("isLastPage");
+                String listStr = alarmJson.getString("list");
+                List<AlarmEventLog> tmp = JSONObject.parseArray(listStr, AlarmEventLog.class);
+
+                list.addAll(tmp);
+
+                fields.clear();
+                pageNo++;
+            }
+            while (!isLastPage);
+        }
+        catch (Exception e) {
+            log.error("getAlarmEventLogList fail!", e);
+        }
+
+        return list;
+    }
+}

+ 45 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/config/BaCtlConfig.java

@@ -0,0 +1,45 @@
+/*
+ * 文 件 名:  InDoorEnergyConfig
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/8/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.ba.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Map;
+
+/**
+ * 室内能耗参数
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/8/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "adapter.ba-ctl")
+public class BaCtlConfig {
+    private String url;
+
+    private String appId;
+
+    private Map<String, String> xfMapper;
+
+    private Map<String, String> ahuMapper;
+
+    private Map<String, String> wtMapper;
+
+    private Map<String, String> wpMapper;
+
+    private Map<String, String> lightMapper;
+}

+ 77 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/controller/BaCtlController.java

@@ -0,0 +1,77 @@
+/*
+ * 文 件 名:  InDoorEnergyController
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/8/18
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.ba.controller;
+
+import com.ruoyi.ems.ba.handle.BaCtlHandler;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * 楼宇采集控制Api
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/8/18]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@RestController
+@CrossOrigin(allowedHeaders = "*", allowCredentials = "false")
+@RequestMapping("/ba")
+@Api(value = "InDoorEnergyController", description = "楼宇采集控制Api")
+public class BaCtlController {
+    /**
+     * 日志
+     */
+    private static final Logger log = LoggerFactory.getLogger(BaCtlController.class);
+
+    @Qualifier("baCtlHandler")
+    @Resource
+    private BaCtlHandler baCtlHandler;
+
+    /**
+     * 常泰室内能耗能力调用
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/ct/abilityCall", method = RequestMethod.POST)
+    @ApiOperation(value = "/ct/abilityCall", notes = "常泰BA能力调用")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public CallResponse<Void> ctAbilityCall(@RequestBody AbilityPayload abilityPayload) {
+        CallResponse<Void> res = null;
+
+        try {
+            res = baCtlHandler.call(abilityPayload);
+        }
+        catch (Exception e) {
+            log.error("ctAbilityCall fail!", e);
+            res = new CallResponse<>(501, "内部错误:" + e.getMessage());
+        }
+
+        return res;
+    }
+}

+ 1191 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/handle/BaCtlHandler.java

@@ -0,0 +1,1191 @@
+package com.ruoyi.ems.ba.handle;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.huashe.common.exception.Assert;
+import com.huashe.common.exception.BusinessException;
+import com.huashe.common.utils.DateUtils;
+import com.ruoyi.common.redis.service.RedisService;
+import com.ruoyi.ems.ba.config.BaCtlConfig;
+import com.ruoyi.ems.ba.retrofit.BaCtlEnergyTemplate;
+import com.ruoyi.ems.domain.ElecMeterH;
+import com.ruoyi.ems.domain.ElecPgSupplyH;
+import com.ruoyi.ems.domain.EmsDevice;
+import com.ruoyi.ems.domain.EmsObjAttrValue;
+import com.ruoyi.ems.domain.FdEnergyPriceConfig;
+import com.ruoyi.ems.domain.MeterDevice;
+import com.ruoyi.ems.domain.WaterMeterH;
+import com.ruoyi.ems.enums.DevObjType;
+import com.ruoyi.ems.common.handle.BaseMeterDevHandler;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.BoundaryObj;
+import com.ruoyi.ems.common.model.CallData;
+import com.ruoyi.ems.model.CallResponse;
+import com.ruoyi.ems.model.Price;
+import com.ruoyi.ems.model.QueryDevice;
+import com.ruoyi.ems.ba.model.CodesVal;
+import com.ruoyi.ems.ba.model.CodesValReq;
+import com.ruoyi.ems.ba.model.CodesValSetReq;
+import com.ruoyi.ems.service.IEmsObjAttrValueService;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * BA楼控处理类
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/8/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Service
+public class BaCtlHandler extends BaseMeterDevHandler {
+    private static final Logger log = LoggerFactory.getLogger(BaCtlHandler.class);
+
+    // 网关模型代码
+    private static final String GATEWAY_MODEL = "M_W4_DEV_BA_GA";
+
+    // 电表测点模型代码
+    private static final String METER_MODEL_E = "M_W4_DEV_BA_METER_E";
+
+    // 水表测点模型代码
+    private static final String METER_MODEL_W = "M_W4_DEV_BA_METER_W";
+
+    // 新风设备模型代码
+    private static final String METER_MODEL_XF = "M_Z020_DEV_BA_XF";
+
+    // 空调设备模型代码
+    private static final String METER_MODEL_AHU = "M_Z020_DEV_BA_AHU";
+
+    // 水箱设备模型代码
+    private static final String METER_MODEL_WT = "M_Z020_DEV_BA_WT";
+
+    // 水泵设备模型代码
+    private static final String METER_MODEL_WP = "M_Z020_DEV_BA_WP";
+
+    // 照明模型代码
+    private static final String METER_MODEL_LIGHT = "M_Z020_DEV_BA_LIGHT";
+
+    // 设备子系统代码
+    private static final String SUBSYSTEM_CODE = "SYS_NHJC";
+
+    // 单小时最大增长量(度)
+    private static final double MAX_HOUR_INCREASE_RATE = 1000.0;
+
+    // 允许的最小负增长(考虑精度误差)
+    private static final double MIN_REASONABLE_DECREASE_RATE = -0.1;
+
+    // 异常数据连续确认次数
+    private static final int ABNORMAL_DATA_CONFIRM_COUNT = 3;
+
+    @Resource
+    private IEmsObjAttrValueService objAttrValueService;
+
+    @Resource
+    private BaCtlConfig config;
+
+    @Autowired
+    private RedisService redisService;
+
+    @Override
+    public List<MeterDevice> getMeterDeviceList() {
+        QueryDevice queryDevice = new QueryDevice();
+        queryDevice.setDeviceModel(METER_MODEL_E);
+        queryDevice.setDeviceEnable(1);
+        List<MeterDevice> list = new ArrayList<>(meterDeviceService.selectMeterDeviceList(queryDevice));
+
+        QueryDevice queryDevice2 = new QueryDevice();
+        queryDevice2.setDeviceModel(METER_MODEL_W);
+        queryDevice2.setDeviceEnable(1);
+        list.addAll(meterDeviceService.selectMeterDeviceList(queryDevice2));
+
+        return list;
+    }
+
+    public List<MeterDevice> getMeterDeviceList(String modelCode) {
+        QueryDevice queryDevice = new QueryDevice();
+        queryDevice.setDeviceModel(modelCode);
+        queryDevice.setDeviceEnable(1);
+        return meterDeviceService.selectMeterDeviceList(queryDevice);
+    }
+
+    @Override
+    public List<EmsDevice> getDeviceList() {
+        throw new UnsupportedOperationException();
+    }
+
+    public List<EmsDevice> getDeviceList(String modelCode) {
+        QueryDevice queryDevice = new QueryDevice();
+        queryDevice.setDeviceModel(modelCode);
+        return deviceService.selectList(queryDevice);
+    }
+
+    @Override
+    public CallResponse<Void> call(AbilityPayload param) {
+        CallResponse<Void> callResponse = null;
+
+        try {
+            if (DevObjType.SYSTEM.getCode() == param.getObjType()) {
+                if (StringUtils.equals("MeterReadingTotal", param.getAbilityKey())) {
+                    List<EmsDevice> deviceList = getDeviceList(GATEWAY_MODEL);
+
+                    for (EmsDevice device : deviceList) {
+                        meterReadingGw(device.getDeviceModel(), "MeterReadingGw", device.getDeviceCode());
+                    }
+                }
+                else if (StringUtils.equals("SyncXfDevAttr", param.getAbilityKey())) {
+                    xfCollect();
+                }
+                else if (StringUtils.equals("SyncAuhDevAttr", param.getAbilityKey())) {
+                    ahuCollect();
+                }
+                else if (StringUtils.equals("SyncWtDevAttr", param.getAbilityKey())) {
+                    wtCollect();
+                }
+                else if (StringUtils.equals("SyncWpDevAttr", param.getAbilityKey())) {
+                    wpCollect();
+                }
+                else if (StringUtils.equals("SyncLightDevAttr", param.getAbilityKey())) {
+                    lightCollect();
+                }
+
+                callResponse = new CallResponse<>(0, "成功");
+            }
+            else if (DevObjType.DEVC.getCode() == param.getObjType()) {
+                if (StringUtils.equals("MeterReadingGw", param.getAbilityKey()) && StringUtils.equals(
+                    param.getModelCode(), GATEWAY_MODEL)) {
+                    meterReadingGw(param.getModelCode(), param.getAbilityKey(), param.getObjCode());
+                }
+                else if (StringUtils.equals("StartStopCtl", param.getAbilityKey()) && StringUtils.equals(
+                    param.getModelCode(), METER_MODEL_XF)) {
+                    Map<String, String> xfMapper = config.getXfMapper();
+                    String pointId = xfMapper.get(String.format("%s.%s", param.getObjCode(), "setCtl-StartStop"));
+                    Assert.notEmpty(pointId, -1, "该设备未配置控制测点pointId.");
+                    devAbilityCall(METER_MODEL_XF, param.getObjCode(), pointId, param.getAbilityKey(),
+                        param.getAbilityParam());
+                }
+                else if (StringUtils.equals("StartStopCtl", param.getAbilityKey()) && StringUtils.equals(
+                    param.getModelCode(), METER_MODEL_AHU)) {
+                    Map<String, String> ahuMapper = config.getAhuMapper();
+                    String pointId = ahuMapper.get(String.format("%s.%s", param.getObjCode(), "setCtl-StartStop"));
+                    Assert.notEmpty(pointId, -1, "该设备未配置控制测点pointId.");
+                    devAbilityCall(METER_MODEL_AHU, param.getObjCode(), pointId, param.getAbilityKey(),
+                        param.getAbilityParam());
+                }
+                else if (StringUtils.equals("OnOffCtl", param.getAbilityKey()) && StringUtils.equals(
+                    param.getModelCode(), METER_MODEL_AHU)) {
+                    Map<String, String> lightMapper = config.getLightMapper();
+                    String pointId = lightMapper.get(String.format("%s.%s", param.getObjCode(), "setCtl-OnOff"));
+                    Assert.notEmpty(pointId, -1, "该设备未配置控制测点pointId.");
+                    devAbilityCall(METER_MODEL_LIGHT, param.getObjCode(), pointId, param.getAbilityKey(),
+                        param.getAbilityParam());
+                }
+
+                callResponse = new CallResponse<>(0, "成功");
+            }
+            else {
+                callResponse = new CallResponse<>(-1, "不支持的能力key:" + param.getAbilityKey());
+            }
+        }
+        catch (BusinessException e) {
+            callResponse = new CallResponse<>(e.getCode(), e.getMessage());
+        }
+
+        return callResponse;
+    }
+
+    @Override
+    public void refreshOnline() {
+    }
+
+    public void devAbilityCall(String modelCode, String deviceId, String pointId, String abilityKey,
+        String paramValue) {
+        CodesVal codesVal = new CodesVal();
+        codesVal.setPointId(pointId);
+        codesVal.setValue(paramValue);
+
+        BaCtlEnergyTemplate template = new BaCtlEnergyTemplate(config.getUrl());
+        CallData<String> callData = template.setCodesVal(new CodesValSetReq(codesVal));
+
+        saveCallLog(deviceId, modelCode, abilityKey, callData.getCallStatus(), callData.getCallPayload(),
+            callData.getResPayload());
+    }
+
+    private void meterReadingGw(String modeCode, String abilityKey, String objCode) {
+        // 查询当前设备的接口属性值
+        EmsObjAttrValue subDevAttr = objAttrValueService.selectObjAttrValue(modeCode, objCode, "subDev");
+
+        if (null != subDevAttr && StringUtils.isNotEmpty(subDevAttr.getAttrValue())) {
+            JSONArray subDevs = JSON.parseArray(subDevAttr.getAttrValue());
+            Set<String> pointIdSet = subDevs.stream().map(item -> ((JSONObject) item).getString("deviceCode"))
+                .collect(Collectors.toSet());
+
+            CodesValReq req = new CodesValReq(pointIdSet);
+
+            // 调用能耗数据接口获取实时数据
+            CallData<String> callData = new BaCtlEnergyTemplate(config.getUrl()).getCodesVal(req);
+            String callRes = callData.getResPayload();
+            JSONObject resJson = JSONObject.parseObject(callRes);
+
+            if (resJson.getInteger("code") == 200) {
+                saveCallLog(objCode, modeCode, abilityKey, 0, JSON.toJSONString(req), callRes);
+
+                String dataStr = resJson.getString("ResultPointObjArr");
+                List<CodesVal> retList = JSON.parseArray(dataStr, CodesVal.class);
+                // 更新设备属性
+                updateMeterDeviceAttrList(objCode, subDevs, retList);
+            }
+            else {
+                saveCallLog(objCode, modeCode, abilityKey, 2, JSON.toJSONString(req), callRes);
+                throw new BusinessException(-1, resJson.getString("error"));
+            }
+        }
+    }
+
+    /**
+     * 采集室内能耗计量数据
+     *
+     * @return 采集条数
+     */
+    public int meterCollect() {
+        int cnt = 0;
+
+        try {
+            List<EmsObjAttrValue> gwDeviceList = objAttrValueService.selectByAttrKey(GATEWAY_MODEL, "subDev");
+
+            // 遍历每个网关设备
+            if (CollectionUtils.isNotEmpty(gwDeviceList)) {
+                // 调用能耗数据接口获取实时数据
+                BaCtlEnergyTemplate template = new BaCtlEnergyTemplate(config.getUrl());
+
+                for (EmsObjAttrValue gwDevice : gwDeviceList) {
+                    cnt += meterDevCollect(template, gwDevice);
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("能耗数据抄报异常", e);
+        }
+
+        return cnt;
+    }
+
+    public void xfCollect() {
+        try {
+            List<EmsDevice> deviceList = getDeviceList(METER_MODEL_XF);
+
+            if (CollectionUtils.isNotEmpty(deviceList)) {
+                for (EmsDevice device : deviceList) {
+                    xfDeviceCollect(device);
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("新风采集异常", e);
+        }
+    }
+
+    public void ahuCollect() {
+        try {
+            List<EmsDevice> deviceList = getDeviceList(METER_MODEL_AHU);
+
+            if (CollectionUtils.isNotEmpty(deviceList)) {
+                for (EmsDevice device : deviceList) {
+                    auhDeviceCollect(device);
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("新风采集异常", e);
+        }
+    }
+
+    public void wtCollect() {
+        try {
+            List<EmsDevice> deviceList = getDeviceList(METER_MODEL_WT);
+
+            if (CollectionUtils.isNotEmpty(deviceList)) {
+                for (EmsDevice device : deviceList) {
+                    wtDeviceCollect(device);
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("水箱采集异常", e);
+        }
+    }
+
+    public void wpCollect() {
+        try {
+            List<EmsDevice> deviceList = getDeviceList(METER_MODEL_WP);
+
+            if (CollectionUtils.isNotEmpty(deviceList)) {
+                for (EmsDevice device : deviceList) {
+                    wpDeviceCollect(device);
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("水泵采集异常", e);
+        }
+    }
+
+    public void lightCollect() {
+        try {
+            List<EmsDevice> deviceList = getDeviceList(METER_MODEL_LIGHT);
+
+            if (CollectionUtils.isNotEmpty(deviceList)) {
+                for (EmsDevice device : deviceList) {
+                    lightDeviceCollect(device);
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("照明采集异常", e);
+        }
+    }
+
+    private int meterDevCollect(BaCtlEnergyTemplate template, EmsObjAttrValue subDevAttr) {
+        int cnt = 0;
+
+        try {
+
+            if (null != subDevAttr && StringUtils.isNotEmpty(subDevAttr.getAttrValue())) {
+                JSONArray subDevs = JSON.parseArray(subDevAttr.getAttrValue());
+                Set<String> pointIdSet = subDevs.stream().map(item -> ((JSONObject) item).getString("deviceCode"))
+                    .collect(Collectors.toSet());
+
+                CodesValReq req = new CodesValReq(pointIdSet);
+
+                // 调用能耗数据接口获取实时数据
+                CallData<String> callData = template.getCodesVal(req);
+                String callRes = callData.getResPayload();
+                JSONObject resJson = JSONObject.parseObject(callRes);
+                Assert.isTrue(StringUtils.equals(resJson.getString("code"), "200"), resJson.getInteger("code"),
+                    resJson.getString("error"));
+                int callStatus = StringUtils.equals(resJson.getString("code"), "200") ? 0 : 2;
+                saveCallLog(subDevAttr.getObjCode(), GATEWAY_MODEL, "MeterReadingGw", callStatus,
+                    JSON.toJSONString(req), callRes);
+
+                String dataStr = resJson.getString("ResultPointObjArr");
+                List<CodesVal> retList = JSON.parseArray(dataStr, CodesVal.class);
+
+                // 更新设备属性
+                updateMeterDeviceAttrList(subDevAttr.getObjCode(), subDevs, retList);
+            }
+        }
+        catch (Exception e) {
+            log.error("meterDevHourProd error! deviceCode:{}", subDevAttr.getObjCode(), e);
+        }
+
+        return cnt;
+    }
+
+    private void xfDeviceCollect(EmsDevice device) {
+        String deviceCode = device.getDeviceCode();
+        Map<String, String> paramKeys = config.getXfMapper();
+
+        Set<String> pointIds = paramKeys.entrySet().stream().filter(
+                entry -> StringUtils.startsWith(entry.getKey(), deviceCode) && !StringUtils.startsWith(entry.getKey(),
+                    deviceCode + ".setCtl-")).flatMap(entry -> Arrays.stream(StringUtils.split(entry.getValue(), ",")))
+            .map(String::trim).collect(Collectors.toSet());
+
+        if (CollectionUtils.isNotEmpty(pointIds)) {
+            CodesValReq req = new CodesValReq(pointIds);
+
+            // 调用能耗数据接口获取实时数据
+            CallData<String> callData = new BaCtlEnergyTemplate(config.getUrl()).getCodesVal(req);
+            String callRes = callData.getResPayload();
+            saveCallLog(device.getDeviceCode(), device.getDeviceModel(), "SyncXfDevAttr", 0, JSON.toJSONString(req), callRes);
+
+            JSONObject resJson = JSONObject.parseObject(callRes);
+            Assert.isTrue(StringUtils.equals(resJson.getString("code"), "200"), resJson.getInteger("code"),
+                resJson.getString("error"));
+            String dataStr = resJson.getString("ResultPointObjArr");
+            List<CodesVal> retList = JSON.parseArray(dataStr, CodesVal.class);
+            Map<String, CodesVal> retMap = retList.stream()
+                .collect(Collectors.toMap(CodesVal::getPointId, Function.identity()));
+
+            List<EmsObjAttrValue> dbAttrList = objAttrValueService.selectByObjCode(METER_MODEL_XF, deviceCode);
+            Map<String, EmsObjAttrValue> dbMap = dbAttrList.stream()
+                .collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, Function.identity()));
+
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "xfTemp", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "sfTemp", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "hfTemp", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "pfTemp", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "ppm", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "wCv", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "xfCv", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "pfCv", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "lwDpAlarm", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "fjUvAlarm", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "afAlarm", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "xfStatus", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "xfMA", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "xfFault", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "pfStatus", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "pfMA", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "pfFault", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "xfTempSetVal", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "LnSu", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "timeSetTag", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_XF, deviceCode, "manualTag", paramKeys, dbMap, retMap);
+            baDevTimeStatueCheckAndUpdate(METER_MODEL_XF, deviceCode, "timeStatus", paramKeys, dbMap, retMap);
+        }
+    }
+
+    private void auhDeviceCollect(EmsDevice device) {
+        String deviceCode = device.getDeviceCode();
+        Map<String, String> paramKeys = config.getAhuMapper();
+
+        Set<String> pointIds = paramKeys.entrySet().stream().filter(
+                entry -> StringUtils.startsWith(entry.getKey(), deviceCode) && !StringUtils.startsWith(entry.getKey(),
+                    deviceCode + ".setCtl-")).flatMap(entry -> Arrays.stream(StringUtils.split(entry.getValue(), ",")))
+            .map(String::trim).collect(Collectors.toSet());
+
+        if (CollectionUtils.isNotEmpty(pointIds)) {
+            CodesValReq req = new CodesValReq(pointIds);
+
+            // 调用能耗数据接口获取实时数据
+            CallData<String> callData = new BaCtlEnergyTemplate(config.getUrl()).getCodesVal(req);
+            String callRes = callData.getResPayload();
+            saveCallLog(device.getDeviceCode(), device.getDeviceModel(), "SyncAuhDevAttr", 0, JSON.toJSONString(req), callRes);
+            JSONObject resJson = JSONObject.parseObject(callRes);
+            Assert.isTrue(StringUtils.equals(resJson.getString("code"), "200"), resJson.getInteger("code"),
+                resJson.getString("error"));
+            String dataStr = resJson.getString("ResultPointObjArr");
+            List<CodesVal> retList = JSON.parseArray(dataStr, CodesVal.class);
+            Map<String, CodesVal> retMap = retList.stream()
+                .collect(Collectors.toMap(CodesVal::getPointId, Function.identity()));
+
+            List<EmsObjAttrValue> dbAttrList = objAttrValueService.selectByObjCode(METER_MODEL_AHU, deviceCode);
+            Map<String, EmsObjAttrValue> dbMap = dbAttrList.stream()
+                .collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, Function.identity()));
+
+            baDevAttrCheckAndUpdate(METER_MODEL_AHU, deviceCode, "xfTemp", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_AHU, deviceCode, "sfTemp", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_AHU, deviceCode, "hfTemp", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_AHU, deviceCode, "wCv", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_AHU, deviceCode, "xfCv", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_AHU, deviceCode, "lwDpAlarm", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_AHU, deviceCode, "fjUvAlarm", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_AHU, deviceCode, "afAlarm", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_AHU, deviceCode, "xfStatus", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_AHU, deviceCode, "xfMA", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_AHU, deviceCode, "xfFault", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_AHU, deviceCode, "xfTempSetVal", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_AHU, deviceCode, "LnSu", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_AHU, deviceCode, "timeSetTag", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_AHU, deviceCode, "manualTag", paramKeys, dbMap, retMap);
+            baDevTimeStatueCheckAndUpdate(METER_MODEL_AHU, deviceCode, "timeStatus", paramKeys, dbMap, retMap);
+        }
+    }
+
+    private void wtDeviceCollect(EmsDevice device) {
+        String deviceCode = device.getDeviceCode();
+        Map<String, String> paramKeys = config.getWtMapper();
+
+        Set<String> pointIds = paramKeys.entrySet().stream()
+            .filter(entry -> StringUtils.startsWith(entry.getKey(), deviceCode))
+            .flatMap(entry -> Arrays.stream(StringUtils.split(entry.getValue(), ","))).map(String::trim)
+            .collect(Collectors.toSet());
+
+        if (CollectionUtils.isNotEmpty(pointIds)) {
+            CodesValReq req = new CodesValReq(pointIds);
+
+            // 调用能耗数据接口获取实时数据
+            CallData<String> callData = new BaCtlEnergyTemplate(config.getUrl()).getCodesVal(req);
+            String callRes = callData.getResPayload();
+            saveCallLog(device.getDeviceCode(), device.getDeviceModel(), "SyncWtDevAttr", 0, JSON.toJSONString(req), callRes);
+            JSONObject resJson = JSONObject.parseObject(callRes);
+            Assert.isTrue(StringUtils.equals(resJson.getString("code"), "200"), resJson.getInteger("code"),
+                resJson.getString("error"));
+            String dataStr = resJson.getString("ResultPointObjArr");
+            List<CodesVal> retList = JSON.parseArray(dataStr, CodesVal.class);
+            Map<String, CodesVal> retMap = retList.stream()
+                .collect(Collectors.toMap(CodesVal::getPointId, Function.identity()));
+
+            List<EmsObjAttrValue> dbAttrList = objAttrValueService.selectByObjCode(METER_MODEL_WT, deviceCode);
+            Map<String, EmsObjAttrValue> dbMap = dbAttrList.stream()
+                .collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, Function.identity()));
+
+            baDevAttrCheckAndUpdate(METER_MODEL_WT, deviceCode, "highLevelAlarm", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_WT, deviceCode, "lowLevelAlarm", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_WT, deviceCode, "tankLevel", paramKeys, dbMap, retMap);
+        }
+    }
+
+    private void wpDeviceCollect(EmsDevice device) {
+        String deviceCode = device.getDeviceCode();
+        Map<String, String> paramKeys = config.getWpMapper();
+
+        Set<String> pointIds = paramKeys.entrySet().stream()
+            .filter(entry -> StringUtils.startsWith(entry.getKey(), deviceCode))
+            .flatMap(entry -> Arrays.stream(StringUtils.split(entry.getValue(), ","))).map(String::trim)
+            .collect(Collectors.toSet());
+
+        if (CollectionUtils.isNotEmpty(pointIds)) {
+            CodesValReq req = new CodesValReq(pointIds);
+
+            // 调用能耗数据接口获取实时数据
+            CallData<String> callData = new BaCtlEnergyTemplate(config.getUrl()).getCodesVal(req);
+            String callRes = callData.getResPayload();
+            saveCallLog(device.getDeviceCode(), device.getDeviceModel(), "SyncWpDevAttr", 0, JSON.toJSONString(req), callRes);
+            JSONObject resJson = JSONObject.parseObject(callRes);
+            Assert.isTrue(StringUtils.equals(resJson.getString("code"), "200"), resJson.getInteger("code"),
+                resJson.getString("error"));
+            String dataStr = resJson.getString("ResultPointObjArr");
+            List<CodesVal> retList = JSON.parseArray(dataStr, CodesVal.class);
+            Map<String, CodesVal> retMap = retList.stream()
+                .collect(Collectors.toMap(CodesVal::getPointId, Function.identity()));
+
+            List<EmsObjAttrValue> dbAttrList = objAttrValueService.selectByObjCode(METER_MODEL_WP, deviceCode);
+            Map<String, EmsObjAttrValue> dbMap = dbAttrList.stream()
+                .collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, Function.identity()));
+
+            baDevAttrCheckAndUpdate(METER_MODEL_WP, deviceCode, "autoState", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_WP, deviceCode, "runningState", paramKeys, dbMap, retMap);
+            baDevAttrCheckAndUpdate(METER_MODEL_WP, deviceCode, "faultState", paramKeys, dbMap, retMap);
+        }
+    }
+
+    private void lightDeviceCollect(EmsDevice device) {
+        String deviceCode = device.getDeviceCode();
+        Map<String, String> paramKeys = config.getLightMapper();
+
+        Set<String> pointIds = paramKeys.entrySet().stream().filter(
+                entry -> StringUtils.startsWith(entry.getKey(), deviceCode) && !StringUtils.startsWith(entry.getKey(),
+                    deviceCode + ".setCtl-")).flatMap(entry -> Arrays.stream(StringUtils.split(entry.getValue(), ",")))
+            .map(String::trim).collect(Collectors.toSet());
+
+        if (CollectionUtils.isNotEmpty(pointIds)) {
+            CodesValReq req = new CodesValReq(pointIds);
+
+            // 调用能耗数据接口获取实时数据
+            CallData<String> callData = new BaCtlEnergyTemplate(config.getUrl()).getCodesVal(req);
+            String callRes = callData.getResPayload();
+            saveCallLog(device.getDeviceCode(), device.getDeviceModel(), "SyncLightDevAttr", 0, JSON.toJSONString(req), callRes);
+            JSONObject resJson = JSONObject.parseObject(callRes);
+            Assert.isTrue(StringUtils.equals(resJson.getString("code"), "200"), resJson.getInteger("code"),
+                resJson.getString("error"));
+            String dataStr = resJson.getString("ResultPointObjArr");
+            List<CodesVal> retList = JSON.parseArray(dataStr, CodesVal.class);
+            Map<String, CodesVal> retMap = retList.stream()
+                .collect(Collectors.toMap(CodesVal::getPointId, Function.identity()));
+
+            List<EmsObjAttrValue> dbAttrList = objAttrValueService.selectByObjCode(METER_MODEL_LIGHT, deviceCode);
+            Map<String, EmsObjAttrValue> dbMap = dbAttrList.stream()
+                .collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, Function.identity()));
+
+            baDevAttrCheckAndUpdate(METER_MODEL_LIGHT, deviceCode, "Switch", paramKeys, dbMap, retMap);
+        }
+    }
+
+    private void baDevAttrCheckAndUpdate(String devModel, String deviceCode, String attrKey,
+        Map<String, String> kvMap, Map<String, EmsObjAttrValue> dbAttrMap, Map<String, CodesVal> retMap) {
+        EmsObjAttrValue dbAttr = dbAttrMap.get(attrKey);
+        String pointId = kvMap.get(String.format("%s.%s", deviceCode, attrKey));
+
+        if (null != dbAttr && retMap.containsKey(pointId)) {
+            CodesVal codesVal = retMap.get(pointId);
+
+            if (!StringUtils.equals(dbAttr.getAttrValue(), codesVal.getValue())) {
+                objAttrValueService.updateObjAttrValue(devModel, deviceCode, attrKey, codesVal.getValue());
+            }
+        }
+        else {
+            if (null != pointId && retMap.containsKey(pointId)) {
+                CodesVal codesVal = retMap.get(pointId);
+                EmsObjAttrValue attr = new EmsObjAttrValue(deviceCode, devModel, attrKey, codesVal.getValue());
+                objAttrValueService.mergeObjAttrValue(attr);
+            }
+        }
+    }
+
+    private void baDevTimeStatueCheckAndUpdate(String devModel, String deviceCode, String attrKey,
+        Map<String, String> kvMap, Map<String, EmsObjAttrValue> dbAttrMap, Map<String, CodesVal> retMap) {
+        EmsObjAttrValue dbAttr = dbAttrMap.get(attrKey);
+        String pointIdStr = kvMap.get(String.format("%s.%s", deviceCode, attrKey));
+
+        if (StringUtils.isNotEmpty(pointIdStr)) {
+            String[] array = StringUtils.split(pointIdStr, ",");
+            String value = "0";
+
+            // 定时开-小时
+            CodesVal codesVal = retMap.get(array[0]);
+
+            if (null != codesVal && StringUtils.equals(codesVal.getValue(), "1")) {
+                value = "160";
+            }
+
+            codesVal = retMap.get(array[1]);
+
+            if (null != codesVal && StringUtils.equals(codesVal.getValue(), "1")) {
+                value = "101";
+            }
+
+            codesVal = retMap.get(array[2]);
+
+            if (null != codesVal && StringUtils.equals(codesVal.getValue(), "1")) {
+                value = "260";
+            }
+
+            codesVal = retMap.get(array[3]);
+
+            if (null != codesVal && StringUtils.equals(codesVal.getValue(), "1")) {
+                value = "201";
+            }
+
+            if (null != dbAttr && !StringUtils.equals(dbAttr.getAttrValue(), value)) {
+                objAttrValueService.updateObjAttrValue(devModel, deviceCode, attrKey, value);
+            }
+            else if (null == dbAttr) {
+                EmsObjAttrValue attr = new EmsObjAttrValue(deviceCode, devModel, attrKey, value);
+                objAttrValueService.mergeObjAttrValue(attr);
+            }
+        }
+    }
+
+    private void updateMeterDeviceAttrList(String gwDevieCode, JSONArray subDevs, List<CodesVal> retList) {
+        try {
+            // 将新采集数据转换为Map
+            Map<String, CodesVal> codesValMap = retList.stream()
+                .collect(Collectors.toMap(CodesVal::getPointId, Function.identity()));
+
+            for (Object subDev : subDevs) {
+                JSONObject subDevObj = (JSONObject) subDev;
+                String pointId = subDevObj.getString("deviceCode");
+                String modelCode = subDevObj.getString("modelCode");
+
+                CodesVal syncCodesVal = codesValMap.get(pointId);
+
+                if (null != syncCodesVal) {
+                    EmsObjAttrValue dbAttr = objAttrValueService.selectObjAttrValue(modelCode, pointId, "value");
+
+                    // 数据验证和处理
+                    DataValidationResult validationResult = validateMeterData(gwDevieCode, pointId,
+                        dbAttr.getAttrValue(), syncCodesVal.getValue());
+
+                    if (validationResult.isValid()) {
+                        // 更新缓存中的数据
+                        String hKey = NEW_HOUR_READING + "-" + pointId;
+                        redisService.setCacheMapValue(gwDevieCode, hKey, validationResult.getValidValue());
+
+                        if (!StringUtils.equals(validationResult.getValidValue(), dbAttr.getAttrValue())) {
+                            objAttrValueService.updateObjAttrValue(modelCode, pointId, "value",
+                                validationResult.getValidValue());
+                        }
+                    }
+                    else {
+                        // 记录异常数据但不更新
+                        log.warn("检测到异常抄表数据 - 设备:{}, 测点:{}, 旧值:{}, 新值:{}, 原因:{}",
+                            dbAttr.getObjCode(), pointId, dbAttr.getAttrValue(), syncCodesVal.getValue(),
+                            validationResult.getReason());
+                    }
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("更新设备属性异常", e);
+        }
+    }
+
+    /**
+     * 能耗数据抄报(扩展支持市电计量)
+     */
+    @Override
+    public int meterHourProd() {
+        int cnt = 0;
+
+        try {
+            // 计量设备-网关设备映射集合
+            Map<String, EmsDevice> refParentMap = getRefParentMap();
+
+            // 电表
+            List<MeterDevice> meterDeviceList = getMeterDeviceList(METER_MODEL_E);
+
+            // 计算电表小时数据
+            if (CollectionUtils.isNotEmpty(meterDeviceList)) {
+                Map<String, Price> priceMap = new ConcurrentHashMap<>();
+                Date date = DateUtils.adjustHour(new Date(), -1);
+                List<ElecMeterH> elecMeterHList = new ArrayList<>();
+
+                for (MeterDevice meterDevice : meterDeviceList) {
+                    EmsDevice gwDevice = refParentMap.get(meterDevice.getDeviceCode());
+                    ElecMeterH elecMeterH = workElecMeterReading(gwDevice, meterDevice);
+
+                    if (null != elecMeterH) {
+                        Price price = priceMap.computeIfAbsent(gwDevice.getAreaCode(),
+                            k -> priceService.getElecHourPrice(gwDevice.getAreaCode(), date));
+                        completeElecPrice(elecMeterH, price);
+                        elecMeterHList.add(elecMeterH);
+                    }
+                }
+
+                if (CollectionUtils.isNotEmpty(elecMeterHList)) {
+                    elecMeterHService.insertBatch(elecMeterHList);
+                    cnt += elecMeterHList.size();
+                    generatePgSupplyData(elecMeterHList);
+                }
+            }
+
+            // 水表
+            meterDeviceList = getMeterDeviceList(METER_MODEL_W);
+
+            // 计算水表小时数据
+            if (CollectionUtils.isNotEmpty(meterDeviceList)) {
+                Map<String, FdEnergyPriceConfig> priceMap = new ConcurrentHashMap<>();
+                List<WaterMeterH> waterMeterHList = new ArrayList<>();
+
+                for (MeterDevice meterDevice : meterDeviceList) {
+                    EmsDevice gwDevice = refParentMap.get(meterDevice.getDeviceCode());
+                    WaterMeterH waterMeterH = workWaterMeterReading(gwDevice, meterDevice);
+
+                    if (null != waterMeterH) {
+                        FdEnergyPriceConfig price = priceMap.computeIfAbsent(gwDevice.getAreaCode(),
+                            k -> fdEnergyPriceConfigService.selectByAreaCode(gwDevice.getAreaCode(), 70));
+                        completeWaterPrice(waterMeterH, price);
+                        waterMeterHList.add(waterMeterH);
+                    }
+                }
+
+                if (CollectionUtils.isNotEmpty(waterMeterHList)) {
+                    waterMeterHService.insertBatch(waterMeterHList);
+                    cnt += waterMeterHList.size();
+
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("能耗数据抄报异常", e);
+        }
+
+        return cnt;
+    }
+
+    private Map<String, EmsDevice> getRefParentMap() {
+        Map<String, EmsDevice> refParentMap = new HashMap<>();
+
+        List<EmsObjAttrValue> attrValues = objAttrValueService.selectByAttrKey(GATEWAY_MODEL, "subDev");
+        Map<String, String> attrValueMap = attrValues.stream()
+            .collect(Collectors.toMap(EmsObjAttrValue::getObjCode, EmsObjAttrValue::getAttrValue));
+
+        // 网关设备列表
+        List<EmsDevice> gwDevices = getDeviceList(GATEWAY_MODEL);
+
+        if (CollectionUtils.isNotEmpty(gwDevices)) {
+            for (EmsDevice gwDevice : gwDevices) {
+                String subDevStr = attrValueMap.get(gwDevice.getDeviceCode());
+                if (StringUtils.isNotBlank(subDevStr)) {
+                    JSONArray jsonArray = JSON.parseArray(subDevStr);
+                    jsonArray.forEach(obj -> {
+                        JSONObject jsonObject = (JSONObject) obj;
+                        refParentMap.put(jsonObject.getString("deviceCode"), gwDevice);
+                    });
+                }
+            }
+        }
+
+        return refParentMap;
+    }
+
+    /**
+     * 修改后的单设备计量产出方法,返回详细结果
+     */
+
+    private ElecMeterH workElecMeterReading(EmsDevice gwDevice, MeterDevice meterDevice) {
+        ElecMeterH elecMeterH = null;
+
+        // 读取最新抄表值
+        String newMeterReading = redisService.getCacheMapValue(gwDevice.getDeviceCode(),
+            NEW_HOUR_READING + "-" + meterDevice.getDeviceCode());
+
+        if (null != newMeterReading) {
+            String lastCacheKey = LAST_HOUR_READING + "-" + meterDevice.getDeviceCode();
+
+            // 读取历史抄表
+            String lastMeterReading = redisService.getCacheMapValue(gwDevice.getDeviceCode(), lastCacheKey);
+
+            elecMeterH = getElecMeterH(gwDevice.getDeviceCode(), meterDevice, lastMeterReading, newMeterReading);
+        }
+
+        return elecMeterH;
+    }
+
+    @Override
+    public ElecMeterH getElecMeterH(String deviceCode, MeterDevice mDevice, String lastMeterReading,
+        String newMeterReading) {
+        ElecMeterH elecMeterH = null;
+        String cacheKey = LAST_HOUR_READING + "-" + mDevice.getDeviceCode();
+        String timestampKey = LAST_HOUR_TIMESTAMP + "-" + mDevice.getDeviceCode();
+
+        // 获取缓存时间戳
+        String cachedTimestamp = redisService.getCacheMapValue(deviceCode, timestampKey);
+        boolean shouldUseCache = false;
+
+        if (StringUtils.isNotEmpty(lastMeterReading) && StringUtils.isNotEmpty(cachedTimestamp)) {
+            try {
+                long lastUpdateTime = Long.parseLong(cachedTimestamp);
+                long currentTime = System.currentTimeMillis();
+                // 只有当缓存数据在2小时内才使用
+                shouldUseCache = (currentTime - lastUpdateTime) <= MAX_CACHE_AGE_MILLIS;
+            }
+            catch (NumberFormatException e) {
+                log.warn("无效的时间戳缓存: {}", cachedTimestamp);
+            }
+        }
+
+        if (shouldUseCache) {
+            // 缓存有效,使用缓存计算
+            elecMeterH = execElecHourMeter(mDevice, lastMeterReading, newMeterReading);
+            // 更新缓存和时间戳
+            updateCacheAfterSuccess(deviceCode, cacheKey, newMeterReading);
+            redisService.setCacheMapValue(deviceCode, timestampKey, String.valueOf(System.currentTimeMillis()));
+        }
+        else {
+            // 缓存过期或无效,查询数据库
+            ElecMeterH dbElecMeterH = elecMeterHService.selectLatelyItem(mDevice.getDeviceCode());
+
+            if (null != dbElecMeterH && null != dbElecMeterH.getMeterReading()) {
+                // 验证数据库记录是否为上一小时
+                Date currentHour = DateUtils.adjustHour(new Date(), -1);
+                long timeDiff = currentHour.getTime() - dbElecMeterH.getRecordTime().getTime();
+
+                // 只有时间差在1小时左右(容差30分钟)才计算
+                if (Math.abs(timeDiff) <= 30 * 60 * 1000) {
+                    elecMeterH = execElecHourMeter(mDevice, String.valueOf(dbElecMeterH.getMeterReading()),
+                        newMeterReading);
+                }
+                else {
+                    log.warn("数据时间差过大,跳过计算 - 设备:{}, 时间差:{}小时", mDevice.getDeviceCode(),
+                        timeDiff / (60 * 60 * 1000));
+                }
+
+                // 无论是否计算,都更新缓存为当前值,作为下次计算基准
+                updateCacheAfterSuccess(deviceCode, cacheKey, newMeterReading);
+                redisService.setCacheMapValue(deviceCode, timestampKey, String.valueOf(System.currentTimeMillis()));
+            }
+            else {
+                // 首次上报,仅更新缓存
+                if (StringUtils.isNotEmpty(newMeterReading)) {
+                    updateCacheAfterSuccess(deviceCode, cacheKey, newMeterReading);
+                    redisService.setCacheMapValue(deviceCode, timestampKey, String.valueOf(System.currentTimeMillis()));
+                }
+            }
+        }
+
+        return elecMeterH;
+    }
+
+    private WaterMeterH workWaterMeterReading(EmsDevice gwDevice, MeterDevice meterDevice) {
+        WaterMeterH waterMeterH = null;
+
+        // 读取最新抄表值
+        String newMeterReading = redisService.getCacheMapValue(gwDevice.getDeviceCode(),
+            NEW_HOUR_READING + "-" + meterDevice.getDeviceCode());
+
+        if (null != newMeterReading) {
+            String lastCacheKey = LAST_HOUR_READING + "-" + meterDevice.getDeviceCode();
+
+            // 读取历史抄表
+            String lastMeterReading = redisService.getCacheMapValue(gwDevice.getDeviceCode(), lastCacheKey);
+
+            // 组装水表抄报数据
+            waterMeterH = getWaterMeterH(gwDevice.getDeviceCode(), meterDevice, lastMeterReading, newMeterReading);
+        }
+
+        return waterMeterH;
+    }
+
+    @Override
+    public WaterMeterH getWaterMeterH(String deviceCode, MeterDevice mDevice, String lastMeterReading,
+        String newMeterReading) {
+        WaterMeterH waterMeterH = null;
+        String cacheKey = LAST_HOUR_READING + "-" + mDevice.getDeviceCode();
+        String timestampKey = LAST_HOUR_TIMESTAMP + "-" + mDevice.getDeviceCode();
+
+        // 获取缓存时间戳
+        String cachedTimestamp = redisService.getCacheMapValue(deviceCode, timestampKey);
+        boolean shouldUseCache = false;
+
+        if (StringUtils.isNotEmpty(lastMeterReading) && StringUtils.isNotEmpty(cachedTimestamp)) {
+            try {
+                long lastUpdateTime = Long.parseLong(cachedTimestamp);
+                long currentTime = System.currentTimeMillis();
+                // 只有当缓存数据在2小时内才使用
+                shouldUseCache = (currentTime - lastUpdateTime) <= MAX_CACHE_AGE_MILLIS;
+
+                if (!shouldUseCache) {
+                    log.warn("水表缓存数据过期 - 设备:{}, 缓存时长:{}小时", mDevice.getDeviceCode(),
+                        (currentTime - lastUpdateTime) / (60 * 60 * 1000));
+                }
+            }
+            catch (NumberFormatException e) {
+                log.warn("无效的时间戳缓存: {}", cachedTimestamp);
+            }
+        }
+
+        if (shouldUseCache) {
+            // 缓存有效,使用缓存计算
+            waterMeterH = execWaterHourMeter(mDevice, lastMeterReading, newMeterReading);
+            if (waterMeterH != null) {
+                // 只有成功计算后才更新缓存
+                updateCacheAfterSuccess(deviceCode, cacheKey, newMeterReading);
+                redisService.setCacheMapValue(deviceCode, timestampKey, String.valueOf(System.currentTimeMillis()));
+            }
+        }
+        else {
+            // 缓存过期或无效,查询数据库
+            WaterMeterH dbWaterMeterH = waterMeterHService.selectLatelyItem(mDevice.getDeviceCode());
+
+            if (null != dbWaterMeterH && null != dbWaterMeterH.getMeterReading()) {
+                // 验证数据库记录是否为上一小时
+                Date currentHour = DateUtils.adjustHour(new Date(), -1);
+                long timeDiff = currentHour.getTime() - dbWaterMeterH.getRecordTime().getTime();
+
+                // 只有时间差在1小时左右(容差30分钟)才计算
+                if (Math.abs(timeDiff) <= 30 * 60 * 1000) {
+                    waterMeterH = execWaterHourMeter(mDevice, String.valueOf(dbWaterMeterH.getMeterReading()),
+                        newMeterReading);
+                }
+                else {
+                    log.warn("水表数据时间差过大,跳过计算 - 设备:{}, 时间差:{}小时", mDevice.getDeviceCode(),
+                        timeDiff / (60 * 60 * 1000));
+                }
+            }
+
+            // 无论是否计算成功,都更新缓存为当前值,作为下次计算基准
+            if (StringUtils.isNotEmpty(newMeterReading)) {
+                updateCacheAfterSuccess(deviceCode, cacheKey, newMeterReading);
+                redisService.setCacheMapValue(deviceCode, timestampKey, String.valueOf(System.currentTimeMillis()));
+            }
+        }
+
+        return waterMeterH;
+    }
+
+    /**
+     * 产出市电供应计量数据(优化版)
+     */
+    private void generatePgSupplyData(List<ElecMeterH> allElecMeterHList) {
+        try {
+            // 生成市电供应数据
+            List<ElecPgSupplyH> pgSupplyHList = new ArrayList<>();
+
+            // 获取所有区域的市电设施及其绑定的计量设备
+            Set<String> areaCodeSet = allElecMeterHList.stream().map(ElecMeterH::getAreaCode)
+                .collect(Collectors.toSet());
+
+            Map<String, Price> priceMap = new ConcurrentHashMap<>();
+            Date recordTime = DateUtils.adjustHour(new Date(), -1);
+
+            // 按区域查询市电设施
+            for (String areaCode : areaCodeSet) {
+                // 查询区域下市电的供应设施(同时获取了设施绑定的表计)
+                List<BoundaryObj> facsMeterBinds = facsService.getFacsWithMeterDev(areaCode, "W", "W2", 45);
+
+                if (CollectionUtils.isNotEmpty(facsMeterBinds)) {
+                    for (BoundaryObj facsMeterBind : facsMeterBinds) {
+                        // 过滤出当前设施绑定表计对应的小时计量数据
+                        List<ElecMeterH> subElecMeterHs = filterBindElecMeterDevs(facsMeterBind, allElecMeterHList);
+
+                        if (CollectionUtils.isNotEmpty(subElecMeterHs)) {
+                            // 直接累加同一设施下的所有电表数据(同一小时电价类型相同)
+                            double totalQuantity = subElecMeterHs.stream().mapToDouble(ElecMeterH::getElecQuantity)
+                                .sum();
+
+                            double totalCost = subElecMeterHs.stream().mapToDouble(ElecMeterH::getUseElecCost).sum();
+
+                            // 精度控制:用量保留1位小数,金额保留2位小数
+                            totalQuantity = formatToDecimalPlaces(totalQuantity, 1);
+                            totalCost = formatToDecimalPlaces(totalCost, 2);
+
+                            // 获取电价信息(同一小时所有表计的电价类型和单价都相同,取第一个即可)
+                            ElecMeterH firstMeter = subElecMeterHs.get(0);
+                            Integer meterType = firstMeter.getMeterType();
+                            Double unitPrice = firstMeter.getMeterUnitPrice();
+
+                            // 创建市电供应记录
+                            ElecPgSupplyH pgSupplyH = new ElecPgSupplyH();
+                            pgSupplyH.setAreaCode(areaCode);
+                            pgSupplyH.setFacsCode(facsMeterBind.getObjCode());
+                            pgSupplyH.setRecordTime(recordTime);
+                            pgSupplyH.setDate(recordTime);
+                            pgSupplyH.setTime(recordTime);
+                            pgSupplyH.setTimeIndex(getHourIndex(recordTime));
+                            pgSupplyH.setMeterType(meterType);
+                            pgSupplyH.setMeterUnitPrice(unitPrice);
+                            pgSupplyH.setUseElecQuantity(totalQuantity);
+                            pgSupplyH.setUseElecCost(totalCost);
+                            pgSupplyHList.add(pgSupplyH);
+
+                        }
+                        else {
+                            Price price = priceMap.computeIfAbsent(areaCode,
+                                k -> priceService.getElecHourPrice(areaCode, recordTime));
+
+                            ElecPgSupplyH pgSupplyH = new ElecPgSupplyH();
+                            pgSupplyH.setAreaCode(areaCode);
+                            pgSupplyH.setFacsCode(facsMeterBind.getObjCode());
+                            pgSupplyH.setRecordTime(recordTime);
+                            pgSupplyH.setDate(recordTime);
+                            pgSupplyH.setTime(recordTime);
+                            pgSupplyH.setTimeIndex(getHourIndex(recordTime));
+                            pgSupplyH.setMeterType(price.getMeterType());
+                            pgSupplyH.setMeterUnitPrice(price.getPriceValue());
+                            pgSupplyH.setUseElecQuantity(0.0);
+                            pgSupplyH.setUseElecCost(0.0);
+                            pgSupplyHList.add(pgSupplyH);
+                        }
+                    }
+                }
+                else {
+                    log.warn("未找到市电设施配置,无法产出市电供应数据");
+                }
+            }
+
+            if (CollectionUtils.isNotEmpty(pgSupplyHList)) {
+                pgSupplyHService.insertBatch(pgSupplyHList);
+            }
+        }
+        catch (Exception e) {
+            log.error("产出市电供应计量数据异常", e);
+        }
+    }
+
+    private List<ElecMeterH> filterBindElecMeterDevs(BoundaryObj facsMeterBind, List<ElecMeterH> allElecMeterHList) {
+        Set<String> bindElecMeterDevs = facsMeterBind.getBindMeterDevs();
+        if (CollectionUtils.isEmpty(bindElecMeterDevs)) {
+            return new ArrayList<>();
+        }
+        return allElecMeterHList.stream().filter(elecMeterH -> bindElecMeterDevs.contains(elecMeterH.getDeviceCode()))
+            .collect(Collectors.toList());
+    }
+
+    /**
+     * 验证抄表数据的合理性
+     */
+    private DataValidationResult validateMeterData(String deviceCode, String pointId, String oldValue,
+        String newValue) {
+        try {
+            // 新数据为空或无效
+            if (StringUtils.isBlank(newValue)) {
+                return DataValidationResult.invalid("新值为空");
+            }
+
+            double newVal = Double.parseDouble(newValue);
+
+            // 首次数据,直接接受
+            if (StringUtils.isBlank(oldValue)) {
+                return DataValidationResult.valid(newValue);
+            }
+
+            double oldVal = Double.parseDouble(oldValue);
+            double difference = newVal - oldVal;
+
+            // 1. 检查是否为明显的向后跳变(新值比旧值小很多)
+            if (difference < MIN_REASONABLE_DECREASE_RATE) {
+                // 检查是否可能是表计重置(新值很小,旧值很大)
+                if (newVal < 100 && oldVal > 10000) {
+                    log.info("检测到可能的表计重置 - 设备:{}, 测点:{}, 旧值:{}, 新值:{}", deviceCode, pointId, oldVal,
+                        newVal);
+                    // 表计重置的情况,需要人工确认,暂时不更新
+                    return DataValidationResult.invalid("疑似表计重置,需人工确认");
+                }
+                else {
+                    return DataValidationResult.invalid(String.format("数据向后跳变,差值:%.2f", difference));
+                }
+            }
+
+            // 2. 检查是否增长过快(可能是数据错误)
+            if (difference > MAX_HOUR_INCREASE_RATE) {
+                return DataValidationResult.invalid(String.format("增长过快,差值:%.2f", difference));
+            }
+
+            // 3. 连续异常数据确认机制
+            if (Math.abs(difference) > MAX_HOUR_INCREASE_RATE * 0.1) { // 超过正常增长的10%认为需要关注
+                String abnormalKey = "ABNORMAL_COUNT_" + pointId;
+                String countStr = redisService.getCacheMapValue(deviceCode, abnormalKey);
+                int abnormalCount = StringUtils.isNotBlank(countStr) ? Integer.parseInt(countStr) : 0;
+
+                abnormalCount++;
+                redisService.setCacheMapValue(deviceCode, abnormalKey, String.valueOf(abnormalCount));
+
+                // 如果连续异常次数太多,需要告警但仍然更新数据
+                if (abnormalCount >= ABNORMAL_DATA_CONFIRM_COUNT) {
+                    log.warn("连续异常数据 - 设备:{}, 测点:{}, 连续{}次异常, 当前差值:{}", deviceCode, pointId,
+                        abnormalCount, difference);
+                    // 重置计数
+                    redisService.setCacheMapValue(deviceCode, abnormalKey, "0");
+                }
+            }
+            else {
+                // 数据正常,清除异常计数
+                String abnormalKey = "ABNORMAL_COUNT_" + pointId;
+                redisService.setCacheMapValue(deviceCode, abnormalKey, "0");
+            }
+
+            return DataValidationResult.valid(newValue);
+
+        }
+        catch (NumberFormatException e) {
+            return DataValidationResult.invalid("数据格式错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 数据验证结果内部类
+     */
+    private static class DataValidationResult {
+        private final boolean valid;
+
+        private final String validValue;
+
+        private final String reason;
+
+        private DataValidationResult(boolean valid, String validValue, String reason) {
+            this.valid = valid;
+            this.validValue = validValue;
+            this.reason = reason;
+        }
+
+        public static DataValidationResult valid(String value) {
+            return new DataValidationResult(true, value, null);
+        }
+
+        public static DataValidationResult invalid(String reason) {
+            return new DataValidationResult(false, null, reason);
+        }
+
+        public boolean isValid() {
+            return valid;
+        }
+
+        public String getValidValue() {
+            return validValue;
+        }
+
+        public String getReason() {
+            return reason;
+        }
+    }
+}

+ 31 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/model/CodesVal.java

@@ -0,0 +1,31 @@
+/*
+ * 文 件 名:  CodesVal
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/8/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.ba.model;
+
+import lombok.Data;
+
+/**
+ * 抄表信息
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/8/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class CodesVal {
+    private String pointId;
+
+    private String time;
+
+    private String value;
+}

+ 42 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/model/CodesValReq.java

@@ -0,0 +1,42 @@
+/*
+ * 文 件 名:  CodesValReq
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/29
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.ba.model;
+
+import lombok.Data;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * WEB TALK 实时数据
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/29]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class CodesValReq {
+    private Set<String> codes;
+
+    public CodesValReq() {
+    }
+
+    public CodesValReq(Set<String> codes) {
+        this.codes = codes;
+    }
+
+    public CodesValReq(String... codes) {
+        this.codes = new HashSet<>(Arrays.asList(codes));
+    }
+}

+ 42 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/model/CodesValSetReq.java

@@ -0,0 +1,42 @@
+/*
+ * 文 件 名:  CodesValSetReq
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/11/22
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.ba.model;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 设置下发
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/11/22]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class CodesValSetReq {
+    private List<CodesVal> aryPoints;
+
+    public CodesValSetReq() {
+    }
+
+    public CodesValSetReq(List<CodesVal> aryPoints) {
+        this.aryPoints = aryPoints;
+    }
+
+    public CodesValSetReq(CodesVal... codesVal) {
+        this.aryPoints = new ArrayList<>(Arrays.asList(codesVal));
+    }
+}

+ 57 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/model/Token.java

@@ -0,0 +1,57 @@
+/*
+ * 文 件 名:  Token
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/8/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.ba.model;
+
+import com.alibaba.fastjson.annotation.JSONField;
+
+/**
+ * Token
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/8/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+public class Token {
+    @JSONField(name = "access_token")
+    private String accessToken;
+
+    @JSONField(name = "refresh_token")
+    private String refreshToken;
+
+    @JSONField(name = "token_type")
+    private String tokenType;
+
+    public String getAccessToken() {
+        return accessToken;
+    }
+
+    public void setAccessToken(String accessToken) {
+        this.accessToken = accessToken;
+    }
+
+    public String getRefreshToken() {
+        return refreshToken;
+    }
+
+    public void setRefreshToken(String refreshToken) {
+        this.refreshToken = refreshToken;
+    }
+
+    public String getTokenType() {
+        return tokenType;
+    }
+
+    public void setTokenType(String tokenType) {
+        this.tokenType = tokenType;
+    }
+}

+ 76 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/retrofit/BaCtlEnergyApi.java

@@ -0,0 +1,76 @@
+/*
+ * 文 件 名:  WebTalk
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/28
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.ba.retrofit;
+
+import com.ruoyi.ems.ba.model.CodesValReq;
+import com.ruoyi.ems.ba.model.CodesValSetReq;
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.GET;
+import retrofit2.http.Header;
+import retrofit2.http.Headers;
+import retrofit2.http.POST;
+import retrofit2.http.Query;
+
+/**
+ * 能耗 BA 照明对接
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/28]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+public interface BaCtlEnergyApi {
+    /**
+     * 获取TOKEN
+     *
+     * @param appId     应用ID
+     * @param grantType 认证方式
+     * @return Token
+     */
+    @GET("/_webtalk/_cur/api/access_token")
+    Call<String> getAccessToken(@Query("appId") String appId, @Query("grant_type") String grantType);
+
+    /**
+     * 刷新TOKEN
+     *
+     * @param appId        应用ID
+     * @param grantType    认证方式
+     * @param refreshToken token
+     * @return Token
+     */
+    @GET("/_webtalk/_cur/api/access_token")
+    Call<String> refreshToken(@Query("appId") String appId, @Query("grant_type") String grantType,
+        @Query("refreshToken") String refreshToken);
+
+    /**
+     * 获取实时数据
+     *
+     * @param appId 应用ID
+     * @param token 认证令牌
+     * @return 实时数据
+     */
+    @Headers({ "Content-Type: application/json" })
+    @POST("/_webtalk/_cur/api/getCodesVal")
+    Call<String> getCodesVal(@Header("appId") String appId, @Header("Token") String token, @Body CodesValReq req);
+
+    /**
+     * 下发设置
+     *
+     * @param appId 应用ID
+     * @param token 认证令牌
+     * @return 实时数据
+     */
+    @Headers({ "Content-Type: application/json" })
+    @POST("/_webtalk/_cur/api/setCodesVal")
+    Call<String> setCodesVal(@Header("appId") String appId, @Header("Token") String token, @Body CodesValSetReq req);
+}

+ 252 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/ba/retrofit/BaCtlEnergyTemplate.java

@@ -0,0 +1,252 @@
+/*
+ * 文 件 名:  InDoorEnergyTemplate
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/8/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.ba.retrofit;
+
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.support.retrofit.Retrofit2ConverterFactory;
+import com.alibaba.fastjson2.JSON;
+import com.huashe.common.exception.Assert;
+import com.ruoyi.common.core.utils.SpringUtils;
+import com.ruoyi.common.redis.service.RedisService;
+import com.ruoyi.ems.ba.config.BaCtlConfig;
+import com.ruoyi.ems.common.retrofit.BaseApiTemplate;
+import com.ruoyi.ems.common.model.CallData;
+import com.ruoyi.ems.ba.model.CodesValReq;
+import com.ruoyi.ems.ba.model.CodesValSetReq;
+import com.ruoyi.ems.ba.model.Token;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.OkHttpClient;
+import org.apache.commons.lang3.StringUtils;
+import retrofit2.Call;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 室内能耗操作模板
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/8/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Slf4j
+public class BaCtlEnergyTemplate extends BaseApiTemplate {
+    /**
+     * 访问地址
+     */
+    protected String url;
+
+    /**
+     * 调用代理
+     */
+    protected final BaCtlEnergyApi api;
+
+    public BaCtlEnergyTemplate(String restUrl) {
+        this.url = restUrl;
+
+        OkHttpClient httpClient = getClient();
+        Retrofit retrofit = new Retrofit.Builder().baseUrl(restUrl)
+            .addConverterFactory(Retrofit2ConverterFactory.create()).client(httpClient).build();
+        this.api = retrofit.create(BaCtlEnergyApi.class);
+    }
+
+    /**
+     * 构造调用模板
+     *
+     * @param restUrl        服务地址 (http://ip:port)
+     * @param connectTimeout 连接超时
+     * @param readTimeout    读取超时
+     * @param writeTimeout   写超时
+     */
+    public BaCtlEnergyTemplate(String restUrl, int connectTimeout, int readTimeout, int writeTimeout) {
+        super.connectTimeout = connectTimeout;
+        super.readTimeout = readTimeout;
+        super.writeTimeout = writeTimeout;
+        this.url = restUrl;
+
+        OkHttpClient httpClient = getClient();
+        Retrofit retrofit = new Retrofit.Builder().baseUrl(restUrl)
+            .addConverterFactory(Retrofit2ConverterFactory.create()).client(httpClient).build();
+        api = retrofit.create(BaCtlEnergyApi.class);
+    }
+
+    public synchronized String getToken() {
+        RedisService redisService = SpringUtils.getBean(RedisService.class);
+        String accessToken = redisService.getCacheObject("INDOOR_ENERGY_ACCESS_TOKEN");
+
+        if (StringUtils.isEmpty(accessToken)) {
+            accessToken = getNewToken();
+        }
+
+        return accessToken;
+    }
+
+    public String getNewToken() {
+        RedisService redisService = SpringUtils.getBean(RedisService.class);
+
+        BaCtlConfig config = SpringUtils.getBean(BaCtlConfig.class);
+        Token token = getAuthToken(config.getAppId());
+        log.info("getAuthToken response:{}", JSONObject.toJSONString(token));
+        redisService.setCacheObject("INDOOR_ENERGY_ACCESS_TOKEN", token.getAccessToken(), 5 * 60L, TimeUnit.SECONDS);
+        redisService.setCacheObject("INDOOR_ENERGY_REFRESH_TOKEN", token.getRefreshToken());
+
+        return token.getAccessToken();
+    }
+
+    public synchronized String refreshToken() {
+        RedisService redisService = SpringUtils.getBean(RedisService.class);
+        String refreshToken = redisService.getCacheObject("INDOOR_ENERGY_REFRESH_TOKEN");
+        BaCtlConfig config = SpringUtils.getBean(BaCtlConfig.class);
+        Token token = StringUtils.isEmpty(refreshToken) ?
+            getAuthToken(config.getAppId()) :
+            refreshToken(config.getAppId(), refreshToken);
+        redisService.setCacheObject("INDOOR_ENERGY_ACCESS_TOKEN", token.getAccessToken(), 5 * 60L, TimeUnit.SECONDS);
+        redisService.setCacheObject("INDOOR_ENERGY_REFRESH_TOKEN", token.getRefreshToken(), 60 * 60L, TimeUnit.SECONDS);
+
+        return token.getAccessToken();
+    }
+
+    /**
+     * 获取Token
+     *
+     * @param appId appId
+     * @return 响应
+     */
+    public Token getAuthToken(String appId) {
+        Token token = null;
+
+        try {
+            // 执行调用
+            Call<String> call = api.getAccessToken(appId, "authorization");
+            Response<String> response = call.execute();
+            log.debug("getAccessToken response:{}", response);
+
+            Assert.notEmpty(response.body(), -1, "获取Token失败");
+            token = JSONObject.parseObject(response.body(), Token.class);
+            Assert.notNull(token, -1, "转换Token失败:\n" + response.body());
+        }
+        catch (Exception e) {
+            log.error("getAccessToken fail!", e);
+        }
+
+        return token;
+    }
+
+    /**
+     * 刷新token
+     *
+     * @param appId        appId
+     * @param refreshToken 缓存token
+     * @return token
+     */
+    public Token refreshToken(String appId, String refreshToken) {
+        Token token = null;
+
+        try {
+            // 执行调用
+            Call<String> call = api.refreshToken(appId, "refresh", refreshToken);
+            Response<String> response = call.execute();
+            log.debug("refreshToken response:{}", response);
+
+            Assert.notEmpty(response.body(), -1, "获取Token失败");
+            token = JSONObject.parseObject(response.body(), Token.class);
+            Assert.notNull(token, -1, "转换Token失败:\n" + response.body());
+        }
+        catch (Exception e) {
+            log.error("refreshToken fail!", e);
+        }
+
+        return token;
+    }
+
+    /**
+     * 获取实时数据
+     *
+     * @param req 请求
+     * @return 实时数据列表
+     */
+    public CallData<String> getCodesVal(CodesValReq req) {
+        CallData.Builder<String> builder = CallData.builder();
+
+        try {
+            String token = getToken();
+            BaCtlConfig config = SpringUtils.getBean(BaCtlConfig.class);
+            // 执行调用
+            builder.addCallParam("appId", config.getAppId()).addCallParam("token", token).addCallParam("req", JSONObject.toJSONString(req));
+            Call<String> call = api.getCodesVal(config.getAppId(), token, req);
+            Response<String> response = call.execute();
+            log.debug("getCodesVal response:{}", response);
+
+            JSONObject resJson = JSONObject.parseObject(response.body());
+
+            if (resJson.getInteger("code") == 200) {
+                builder.setCallStatus(0).setResPayload(response.body());
+            }
+            else if (resJson.getInteger("code") == 302) {
+                token = getNewToken();
+                // 执行调用
+                call = api.getCodesVal(config.getAppId(), token, req);
+                response = call.execute();
+                log.debug("getCodesVal response:{}", response);
+                builder.setCallStatus(0).setResPayload(response.body());
+            }
+        }
+        catch (Exception e) {
+            builder.setCallStatus(1);
+            log.error("getCodesVal fail!", e);
+        }
+
+        return builder.build();
+    }
+
+    /**
+     * 设置下发
+     *
+     * @param req 请求
+     * @return 能耗数据列表
+     */
+    public CallData<String> setCodesVal(CodesValSetReq req) {
+        CallData.Builder<String> builder = CallData.builder();
+
+        try {
+            String token = getToken();
+            BaCtlConfig config = SpringUtils.getBean(BaCtlConfig.class);
+            // 执行调用
+            builder.addCallParam("appId", config.getAppId()).addCallParam("token", token).addCallParam("req", JSON.toJSONString(req));
+            Call<String> call = api.setCodesVal(config.getAppId(), token, req);
+            Response<String> response = call.execute();
+            log.debug("setCodesVal response:{}", response);
+
+            JSONObject resJson = JSONObject.parseObject(response.body());
+
+            if (resJson.getInteger("code") == 200) {
+                builder.setCallStatus(0).setResPayload(response.body());
+            }
+            else if (resJson.getInteger("code") == 302) {
+                token = getNewToken();
+                // 执行调用
+                call = api.setCodesVal(config.getAppId(), token, req);
+                response = call.execute();
+                log.debug("setCodesVal response:{}", response);
+                builder.setCallStatus(0).setResPayload(response.body());
+            }
+        }
+        catch (Exception e) {
+            builder.setCallStatus(1);
+            log.error("setCodesVal fail!", e);
+        }
+
+        return builder.build();
+    }
+}

+ 164 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/controller/ChargingController.java

@@ -0,0 +1,164 @@
+/*
+ * 文 件 名:  ChargingController
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩适配控制器
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/25
+ * 修改内容:  新建充电桩能力调用API入口
+ */
+package com.ruoyi.ems.charging.controller;
+
+import com.ruoyi.ems.charging.config.ChargingPileServer;
+import com.ruoyi.ems.charging.core.ChargingPileSessionManager;
+import com.ruoyi.ems.charging.handle.ChargingHandler;
+import com.ruoyi.ems.charging.service.ChargingDataService;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * 充电桩适配控制器
+ * 提供充电桩系统、主机、充电枪的能力调用API
+ * <h3>支持的能力列表:</h3>
+ * <h4>系统级能力 (M_W2_SYS_CHARGING, objType=3):</h4>
+ * <ul>
+ *   <li>setAllPilesPowerLimit - 批量设置功率限制,参数: 30-100</li>
+ *   <li>stopAllCharging - 全部停止充电</li>
+ *   <li>clearCache - 清除设备编码缓存</li>
+ *   <li>getCacheStats - 获取缓存统计信息</li>
+ * </ul>
+ * <h4>充电主机能力 (M_W2_DEV_CHARGING_HOST, objType=2):</h4>
+ * <ul>
+ *   <li>setMaxOutputPower - 设置最大输出功率,参数: 30-100</li>
+ *   <li>enablePile - 启用充电桩</li>
+ *   <li>disablePile - 禁用充电桩</li>
+ * </ul>
+ * <h4>充电枪能力 (M_W2_DEV_CHARGING_PILE, objType=2):</h4>
+ * <ul>
+ *   <li>remoteStop - 远程停止充电</li>
+ *   <li>readRealtimeData - 主动读取实时数据</li>
+ * </ul>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/25]
+ */
+@RestController
+@CrossOrigin(allowedHeaders = "*", allowCredentials = "false")
+@RequestMapping("/charging")
+@Api(value = "ChargingController", description = "充电桩适配接口")
+public class ChargingController {
+
+    private static final Logger log = LoggerFactory.getLogger(ChargingController.class);
+
+    @Resource
+    private ChargingHandler chargingHandler;
+
+    @Resource
+    private ChargingDataService chargingDataService;
+
+    @Resource
+    private ChargingPileServer chargingPileServer;
+
+    @Resource
+    private ChargingPileSessionManager sessionManager;
+
+    /**
+     * 充电桩能力调用入口
+     *
+     * @param abilityPayload 能力调用参数
+     * @return 调用结果
+     */
+    @PostMapping("/ct/abilityCall")
+    @ApiOperation(value = "充电桩能力调用", notes = "统一的充电桩能力调用入口")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public CallResponse<Void> abilityCall(@RequestBody AbilityPayload abilityPayload) {
+        CallResponse<Void> res;
+
+        try {
+            log.info("收到充电桩能力调用请求: {}", abilityPayload);
+            res = chargingHandler.call(abilityPayload);
+        }
+        catch (Exception e) {
+            log.error("充电桩能力调用失败!", e);
+            res = new CallResponse<>(501, "内部错误:" + e.getMessage());
+        }
+
+        return res;
+    }
+
+    /**
+     * 清除设备编码缓存
+     *
+     * @return 操作结果
+     */
+    @PostMapping("/cache/clear")
+    @ApiOperation(value = "清除缓存", notes = "清除设备编码缓存,设备配置变更后需要调用")
+    public CallResponse<Void> clearCache() {
+        try {
+            chargingDataService.clearDeviceCodeCache();
+            return new CallResponse<>(0, "缓存已清除");
+        }
+        catch (Exception e) {
+            log.error("清除缓存失败", e);
+            return new CallResponse<>(-1, e.getMessage());
+        }
+    }
+
+    /**
+     * 刷新充电桩在线状态
+     *
+     * @return 操作结果
+     */
+    @PostMapping("/system/refreshOnline")
+    @ApiOperation(value = "刷新在线状态", notes = "刷新所有充电桩的在线状态")
+    public CallResponse<Void> refreshOnline() {
+        try {
+            chargingHandler.refreshOnline();
+            return new CallResponse<>(0, "在线状态刷新完成");
+        }
+        catch (Exception e) {
+            log.error("刷新在线状态失败", e);
+            return new CallResponse<>(-1, e.getMessage());
+        }
+    }
+
+    /**
+     * 获取服务器状态和缓存统计信息
+     *
+     * @return 状态信息
+     */
+    @GetMapping("/system/status")
+    @ApiOperation(value = "获取系统状态", notes = "获取充电桩服务器状态和缓存统计信息")
+    public CallResponse<String> getSystemStatus() {
+        try {
+            StringBuilder sb = new StringBuilder();
+            sb.append(chargingPileServer.getStatusInfo());
+            sb.append("\n");
+            sb.append("会话管理器状态:\n");
+            sb.append("  在线桩数: ").append(sessionManager.getOnlineCount()).append("\n");
+            sb.append("  总会话数: ").append(sessionManager.getTotalSessionCount()).append("\n");
+            sb.append("  缓存统计: ").append(chargingDataService.getCacheStats()).append("\n");
+            
+            return new CallResponse<>(0, sb.toString());
+        }
+        catch (Exception e) {
+            log.error("获取系统状态失败", e);
+            return new CallResponse<>(-1, e.getMessage());
+        }
+    }
+}

+ 1 - 1
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/core/ChargingPileChannelInitializer.java

@@ -7,7 +7,7 @@
  */
 package com.ruoyi.ems.charging.core;
 
-import com.ruoyi.ems.charging.handler.ChargingPileMessageHandler;
+import com.ruoyi.ems.charging.handle.ChargingPileMessageHandler;
 import io.netty.channel.ChannelInitializer;
 import io.netty.channel.ChannelPipeline;
 import io.netty.channel.socket.SocketChannel;

+ 428 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/handle/ChargingHandler.java

@@ -0,0 +1,428 @@
+/*
+ * 文 件 名:  ChargingHandler
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩适配处理Handler
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2026/1/9
+ */
+package com.ruoyi.ems.charging.handle;
+
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.ems.charging.service.ChargingDeviceMappingCache;
+import com.ruoyi.ems.charging.core.ChargingPileSessionManager;
+import com.ruoyi.ems.charging.service.ChargingDataService;
+import com.ruoyi.ems.charging.service.PowerControlService;
+import com.ruoyi.ems.domain.EmsDevice;
+import com.ruoyi.ems.domain.EmsObjAttrValue;
+import com.ruoyi.ems.enums.DevObjType;
+import com.ruoyi.ems.enums.DevOnlineStatus;
+import com.ruoyi.ems.common.handle.BaseDevHandler;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import com.ruoyi.ems.model.QueryDevice;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 充电桩适配处理Handler
+ * 处理充电桩系统、主机、充电枪的能力调用
+ *
+ * @author lvwenbin
+ * @version [版本号, 2026/1/9]
+ */
+@Service("chargingHandler")
+public class ChargingHandler extends BaseDevHandler {
+
+    private static final Logger log = LoggerFactory.getLogger(ChargingHandler.class);
+
+    /**
+     * 充电系统模型代码
+     */
+    private static final String MODEL_CODE_SYS = "M_W2_SYS_CHARGING";
+
+    /**
+     * 充电主机模型代码
+     */
+    private static final String MODEL_CODE_HOST = "M_W2_DEV_CHARGING_HOST";
+
+    /**
+     * 充电枪模型代码
+     */
+    private static final String MODEL_CODE_PILE = "M_W2_DEV_CHARGING_PILE";
+
+    /**
+     * 子系统代码
+     */
+    private static final String SUBSYSTEM_CODE = "SYS_CD";
+
+    @Autowired
+    private PowerControlService powerControlService;
+
+    @Autowired
+    private ChargingDataService chargingDataService;
+
+    @Autowired
+    private ChargingPileSessionManager sessionManager;
+
+    @Autowired
+    private ChargingDeviceMappingCache deviceMappingCache;
+
+    @Override
+    public CallResponse<Void> call(AbilityPayload abilityParam) {
+        CallResponse<Void> callResponse;
+
+        try {
+            log.info("充电桩能力调用: {}", abilityParam);
+
+            int objType = abilityParam.getObjType();
+            String modelCode = abilityParam.getModelCode();
+            String abilityKey = abilityParam.getAbilityKey();
+            String objCode = abilityParam.getObjCode();
+            String param = abilityParam.getAbilityParam();
+
+            // 根据对象类型分发处理
+            if (DevObjType.SYSTEM.getCode() == objType) {
+                // 系统级能力
+                callResponse = handleSystemAbility(abilityKey, param);
+            }
+            else if (DevObjType.DEVC.getCode() == objType) {
+                // 设备级能力
+                if (MODEL_CODE_HOST.equals(modelCode)) {
+                    callResponse = handleHostAbility(objCode, abilityKey, param);
+                }
+                else if (MODEL_CODE_PILE.equals(modelCode)) {
+                    callResponse = handlePileAbility(objCode, abilityKey, param);
+                }
+                else {
+                    callResponse = new CallResponse<>(-1, "不支持的模型代码: " + modelCode);
+                }
+            }
+            else {
+                callResponse = new CallResponse<>(-1, "不支持的对象类型: " + objType);
+            }
+
+            // 记录调用日志
+            saveCallLog(objCode, modelCode, abilityKey, callResponse.getCode() == 0 ? 0 : -1, param,
+                callResponse.getMessage());
+
+        }
+        catch (Exception e) {
+            log.error("充电桩能力调用异常", e);
+            callResponse = new CallResponse<>(-1, "内部错误: " + e.getMessage());
+        }
+
+        return callResponse;
+    }
+
+    /**
+     * 处理系统级能力
+     */
+    private CallResponse<Void> handleSystemAbility(String abilityKey, String param) {
+        switch (abilityKey) {
+            case "setAllPilesPowerLimit":
+                // 批量设置功率限制
+                int powerPercent = param != null ? Integer.parseInt(param) : 100;
+                if (powerPercent < 30 || powerPercent > 100) {
+                    return new CallResponse<>(-1, "功率百分比需在30-100之间");
+                }
+                powerControlService.setAllPilesPowerLimit(powerPercent);
+                return new CallResponse<>(0, "批量功率限制指令已发送");
+
+            case "stopAllCharging":
+                // 全部停止充电
+                int stopCount = powerControlService.stopAllCharging();
+                return new CallResponse<>(0, "已发送停止充电指令: " + stopCount + "条");
+
+            case "clearCache":
+                chargingDataService.clearDeviceCodeCache();
+                return new CallResponse<>(0, "设备映射缓存已刷新");
+
+            case "getCacheStats":
+                // 获取缓存统计
+                return new CallResponse<>(0, JSON.toJSONString(chargingDataService.getCacheStats()));
+
+            default:
+                return new CallResponse<>(-1, "不支持的系统能力: " + abilityKey);
+        }
+    }
+
+    /**
+     * 处理充电主机能力
+     */
+    private CallResponse<Void> handleHostAbility(String objCode, String abilityKey, String param) {
+        String pileCode = getPileCodeByDeviceCode(objCode);
+        if (StringUtils.isBlank(pileCode)) {
+            return new CallResponse<>(-1, "未找到设备的桩编码: " + objCode);
+        }
+
+        switch (abilityKey) {
+            case "setMaxOutputPower":
+                // 设置最大输出功率
+                int powerPercent = param != null ? Integer.parseInt(param) : 100;
+                if (powerPercent < 30 || powerPercent > 100) {
+                    return new CallResponse<>(-1, "功率百分比需在30-100之间");
+                }
+                boolean powerResult = powerControlService.setMaxOutputPower(pileCode, powerPercent);
+                return powerResult ?
+                    new CallResponse<>(0, "功率设置指令已发送") :
+                    new CallResponse<>(-1, "功率设置指令发送失败,请检查设备是否在线");
+
+            case "enablePile":
+                // 启用充电桩
+                boolean enableResult = powerControlService.enablePile(pileCode);
+                return enableResult ?
+                    new CallResponse<>(0, "启用指令已发送") :
+                    new CallResponse<>(-1, "启用指令发送失败");
+
+            case "disablePile":
+                // 禁用充电桩
+                boolean disableResult = powerControlService.disablePile(pileCode);
+                return disableResult ?
+                    new CallResponse<>(0, "禁用指令已发送") :
+                    new CallResponse<>(-1, "禁用指令发送失败");
+
+            default:
+                return new CallResponse<>(-1, "不支持的主机能力: " + abilityKey);
+        }
+    }
+
+    /**
+     * 处理充电枪能力
+     */
+    private CallResponse<Void> handlePileAbility(String objCode, String abilityKey, String param) {
+        String pileCode = getGunPileCode(objCode);
+        String gunNo = getGunNo(objCode);
+
+        if (StringUtils.isBlank(pileCode) || StringUtils.isBlank(gunNo)) {
+            return new CallResponse<>(-1, "未找到设备的桩编码或枪号: " + objCode);
+        }
+
+        switch (abilityKey) {
+            case "remoteStop":
+                // 远程停止充电
+                boolean stopResult = powerControlService.remoteStop(pileCode, gunNo);
+                return stopResult ?
+                    new CallResponse<>(0, "远程停机指令已发送") :
+                    new CallResponse<>(-1, "远程停机指令发送失败,请检查设备是否在线");
+
+            case "readRealtimeData":
+                // 主动读取实时数据
+                boolean readResult = powerControlService.readRealtimeData(pileCode, gunNo);
+                return readResult ?
+                    new CallResponse<>(0, "读取数据指令已发送") :
+                    new CallResponse<>(-1, "读取数据指令发送失败");
+
+            default:
+                return new CallResponse<>(-1, "不支持的充电枪能力: " + abilityKey);
+        }
+    }
+
+    /**
+     * 根据设备编码获取桩编码(充电主机)
+     */
+    private String getPileCodeByDeviceCode(String deviceCode) {
+        String protocolCode = deviceMappingCache.getProtocolCode(deviceCode);
+
+        // 验证是否为主机编码(14位)
+        if (protocolCode != null) {
+            return protocolCode;
+        }
+
+        log.warn("设备 {} 的协议编码不是有效的桩编码: {}", deviceCode, protocolCode);
+        return null;
+    }
+
+    /**
+     * 获取充电枪的桩编码
+     */
+    private String getGunPileCode(String deviceCode) {
+        String fullGunNo = deviceMappingCache.getProtocolCode(deviceCode);
+
+        // 提取桩编码(前14位)
+        if (fullGunNo != null && fullGunNo.length() == 16) {
+            return fullGunNo.substring(0, 14);
+        }
+
+        log.warn("设备 {} 的协议编码不是有效的完整枪号: {}", deviceCode, fullGunNo);
+        return null;
+    }
+
+    /**
+     * 获取充电枪的枪号
+     */
+    private String getGunNo(String deviceCode) {
+        String fullGunNo = deviceMappingCache.getProtocolCode(deviceCode);
+
+        // 提取枪号(后2位)
+        if (fullGunNo != null && fullGunNo.length() == 16) {
+            return fullGunNo.substring(14);
+        }
+
+        log.warn("设备 {} 的协议编码不是有效的完整枪号: {}", deviceCode, fullGunNo);
+        return null;
+    }
+
+    @Override
+    public List<EmsDevice> getDeviceList() {
+        // 返回所有充电主机设备
+        QueryDevice queryDevice = new QueryDevice();
+        queryDevice.setDeviceModel(MODEL_CODE_HOST);
+        queryDevice.setSubsystemCode(SUBSYSTEM_CODE);
+        return deviceService.selectList(queryDevice);
+    }
+
+    @Override
+    public void refreshOnline() {
+        log.info("开始刷新充电桩在线状态...");
+
+        try {
+            // 查询本地充电主机设备
+            QueryDevice queryHost = new QueryDevice();
+            queryHost.setDeviceModel(MODEL_CODE_HOST);
+            queryHost.setSubsystemCode(SUBSYSTEM_CODE);
+            List<EmsDevice> hostDevices = deviceService.selectList(queryHost);
+
+            for (EmsDevice device : hostDevices) {
+                try {
+                    String pileCode = getPileCodeByDeviceCode(device.getDeviceCode());
+                    if (StringUtils.isBlank(pileCode)) {
+                        continue;
+                    }
+
+                    // 通过SessionManager判断桩是否在线
+                    boolean isOnline = sessionManager.isOnline(pileCode);
+                    DevOnlineStatus newStatus = isOnline ? DevOnlineStatus.ONLINE : DevOnlineStatus.OFFLINE;
+
+                    // 更新状态
+                    refreshStatus(device, newStatus);
+
+                    // 同步更新该主机下所有充电枪的状态
+                    updateGunOnlineStatus(device.getDeviceCode(), isOnline);
+
+                }
+                catch (Exception e) {
+                    log.error("刷新充电主机 {} 状态异常", device.getDeviceCode(), e);
+                }
+            }
+
+            log.info("充电桩在线状态刷新完成");
+
+        }
+        catch (Exception e) {
+            log.error("刷新充电桩在线状态异常", e);
+        }
+    }
+
+    /**
+     * 更新充电枪在线状态
+     */
+    private void updateGunOnlineStatus(String hostDeviceCode, boolean isOnline) {
+        try {
+            String pileCode = deviceMappingCache.getProtocolCode(hostDeviceCode);
+            if (StringUtils.isBlank(pileCode)) {
+                return;
+            }
+
+            ChargingDeviceMappingCache.HostDeviceMapping hostMapping =
+                deviceMappingCache.getHostMapping(pileCode);
+
+            if (hostMapping == null || CollectionUtils.isEmpty(hostMapping.getSubDeviceCodes())) {
+                return;
+            }
+
+            for (String gunDeviceCode : hostMapping.getSubDeviceCodes()) {
+                // 查询枪设备
+                EmsDevice gunDevice = deviceService.selectByCode(gunDeviceCode);
+                if (gunDevice != null) {
+                    DevOnlineStatus newStatus = isOnline ? DevOnlineStatus.ONLINE : DevOnlineStatus.OFFLINE;
+                    refreshStatus(gunDevice, newStatus);
+                }
+            }
+
+        }
+        catch (Exception e) {
+            log.error("更新充电枪在线状态异常 - 主机: {}", hostDeviceCode, e);
+        }
+    }
+
+    /**
+     * 同步单个主机属性
+     */
+    public void syncHostAttr(String deviceCode) {
+        log.info("同步充电主机 {} 属性", deviceCode);
+
+        try {
+            // 使用统一缓存获取桩编码
+            String pileCode = getPileCodeByDeviceCode(deviceCode);
+            if (StringUtils.isBlank(pileCode)) {
+                log.warn("充电主机 {} 无对应桩编码", deviceCode);
+                return;
+            }
+
+            // 检查桩是否在线
+            if (!sessionManager.isOnline(pileCode)) {
+                log.warn("充电桩 {} 不在线,无法同步属性", pileCode);
+                return;
+            }
+
+            // 获取会话信息
+            ChargingPileSessionManager.PileSession session = sessionManager.getSession(pileCode);
+            if (session == null) {
+                return;
+            }
+
+            // 获取现有属性
+            List<EmsObjAttrValue> existingAttrs = objAttrValueService.selectByObjCode(MODEL_CODE_HOST, deviceCode);
+            Map<String, EmsObjAttrValue> attrMap = existingAttrs.stream()
+                .collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, Function.identity(), (k1, k2) -> k1));
+
+            // 更新枪数量
+            checkAndUpdate(attrMap, deviceCode, MODEL_CODE_HOST, "gunCount", String.valueOf(session.getGunCount()));
+
+            log.info("充电主机 {} 属性同步完成", deviceCode);
+
+        }
+        catch (Exception e) {
+            log.error("同步充电主机 {} 属性异常", deviceCode, e);
+        }
+    }
+
+    /**
+     * 同步单个充电枪属性
+     */
+    public void syncGunAttr(String deviceCode) {
+        log.info("同步充电枪 {} 属性", deviceCode);
+
+        try {
+            String pileCode = getGunPileCode(deviceCode);
+            String gunNo = getGunNo(deviceCode);
+
+            if (StringUtils.isBlank(pileCode) || StringUtils.isBlank(gunNo)) {
+                log.warn("充电枪 {} 无pileCode或gunNo属性", deviceCode);
+                return;
+            }
+
+            // 发送读取实时数据指令
+            boolean result = powerControlService.readRealtimeData(pileCode, gunNo);
+            if (result) {
+                log.info("充电枪 {} 属性同步指令已发送", deviceCode);
+            }
+            else {
+                log.warn("充电枪 {} 属性同步指令发送失败", deviceCode);
+            }
+
+        }
+        catch (Exception e) {
+            log.error("同步充电枪 {} 属性异常", deviceCode, e);
+        }
+    }
+}

+ 302 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/charging/handle/ChargingPileMessageHandler.java

@@ -0,0 +1,302 @@
+/*
+ * 文 件 名:  ChargingPileMessageHandler
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  充电桩消息处理器
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/25
+ * 修改内容:
+ *   1. 添加错误报文、交易记录、远程停机应答的处理
+ *   2. 【新增】在channelInactive中调用ChargingDataService.onPileDisconnect更新设备离线状态
+ */
+package com.ruoyi.ems.charging.handle;
+
+import com.ruoyi.ems.charging.core.ChargingPileSessionManager;
+import com.ruoyi.ems.charging.model.BaseFrame;
+import com.ruoyi.ems.charging.model.req.ChargingEndFrame;
+import com.ruoyi.ems.charging.model.req.ChargingHandshakeFrame;
+import com.ruoyi.ems.charging.model.req.ErrorReportFrame;
+import com.ruoyi.ems.charging.model.req.HeartbeatReqFrame;
+import com.ruoyi.ems.charging.model.req.LoginReqFrame;
+import com.ruoyi.ems.charging.model.req.RealtimeDataFrame;
+import com.ruoyi.ems.charging.model.req.RemoteStopRespFrame;
+import com.ruoyi.ems.charging.model.req.TransactionRecordFrame;
+import com.ruoyi.ems.charging.model.req.WorkParamSetRespFrame;
+import com.ruoyi.ems.charging.model.resp.HeartbeatRespFrame;
+import com.ruoyi.ems.charging.model.resp.LoginRespFrame;
+import com.ruoyi.ems.charging.model.resp.TransactionConfirmFrame;
+import com.ruoyi.ems.charging.protocol.ProtocolConstants;
+import com.ruoyi.ems.charging.service.ChargingDataService;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 充电桩消息处理器
+ * 处理充电桩上送的各类协议消息
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/25]
+ */
+@Component
+@ChannelHandler.Sharable
+public class ChargingPileMessageHandler extends SimpleChannelInboundHandler<BaseFrame> {
+
+    private static final Logger log = LoggerFactory.getLogger(ChargingPileMessageHandler.class);
+
+    @Autowired
+    private ChargingPileSessionManager sessionManager;
+
+    @Autowired
+    private ChargingDataService chargingDataService;
+
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, BaseFrame frame) throws Exception {
+        byte frameType = frame.getFrameType();
+
+        switch (frameType) {
+            // 登录认证 (0x01)
+            case ProtocolConstants.FRAME_TYPE_LOGIN_REQ:
+                handleLoginRequest(ctx, (LoginReqFrame) frame);
+                break;
+
+            // 心跳请求 (0x03)
+            case ProtocolConstants.FRAME_TYPE_HEARTBEAT_REQ:
+                handleHeartbeatRequest(ctx, (HeartbeatReqFrame) frame);
+                break;
+
+            // 实时监测数据 (0x13)
+            case ProtocolConstants.FRAME_TYPE_REALTIME_DATA:
+                handleRealtimeData(ctx, (RealtimeDataFrame) frame);
+                break;
+
+            // 充电握手 (0x15)
+            case ProtocolConstants.FRAME_TYPE_CHARGING_HANDSHAKE:
+                handleChargingHandshake(ctx, (ChargingHandshakeFrame) frame);
+                break;
+
+            // 充电结束 (0x19)
+            case ProtocolConstants.FRAME_TYPE_CHARGING_END:
+                handleChargingEnd(ctx, (ChargingEndFrame) frame);
+                break;
+
+            // 错误报文 (0x1B)
+            case ProtocolConstants.FRAME_TYPE_ERROR:
+                handleErrorReport(ctx, (ErrorReportFrame) frame);
+                break;
+
+            // 远程停机命令回复 (0x35)
+            case ProtocolConstants.FRAME_TYPE_REMOTE_STOP_RESP:
+                handleRemoteStopResp(ctx, (RemoteStopRespFrame) frame);
+                break;
+
+            // 交易记录 (0x3B)
+            case ProtocolConstants.FRAME_TYPE_TRANSACTION:
+                handleTransactionRecord(ctx, (TransactionRecordFrame) frame);
+                break;
+
+            // 工作参数设置应答 (0x51)
+            case ProtocolConstants.FRAME_TYPE_WORK_PARAM_SET_RESP:
+                handleWorkParamSetResp(ctx, (WorkParamSetRespFrame) frame);
+                break;
+
+            default:
+                log.info("收到未处理的帧类型: 0x{}", String.format("%02X", frameType & 0xFF));
+                break;
+        }
+    }
+
+    /**
+     * 处理登录认证请求 (0x01)
+     */
+    private void handleLoginRequest(ChannelHandlerContext ctx, LoginReqFrame req) {
+        log.info("收到登录认证请求: {}", req);
+
+        String pileCode = req.getPileCode();
+        int gunCount = req.getGunCount();
+
+        // 验证桩信息(这里简化处理,实际应查询数据库验证)
+        boolean valid = validatePile(pileCode);
+
+        // 发送登录应答
+        LoginRespFrame resp;
+        if (valid) {
+            // 注册会话
+            sessionManager.registerSession(pileCode, ctx.channel(), gunCount);
+            resp = LoginRespFrame.success(pileCode, req.getSequenceNo());
+            log.info("充电桩[{}]登录成功,枪数量: {}", pileCode, gunCount);
+        } else {
+            resp = LoginRespFrame.fail(pileCode, req.getSequenceNo());
+            log.warn("充电桩[{}]登录失败,验证未通过", pileCode);
+        }
+
+        ctx.writeAndFlush(resp);
+
+        // 通知业务层(会更新主机和子设备的在线状态)
+        chargingDataService.onPileLogin(req, valid);
+    }
+
+    /**
+     * 处理心跳请求 (0x03)
+     */
+    private void handleHeartbeatRequest(ChannelHandlerContext ctx, HeartbeatReqFrame req) {
+        log.debug("收到心跳请求: {}", req);
+
+        String pileCode = req.getPileCode();
+
+        // 更新心跳时间
+        sessionManager.updateHeartbeat(pileCode);
+
+        // 发送心跳应答
+        HeartbeatRespFrame resp = HeartbeatRespFrame.create(pileCode, req.getGunNo(), req.getSequenceNo());
+        ctx.writeAndFlush(resp);
+
+        // 如果枪状态异常,通知业务层
+        if (!req.isNormal()) {
+            log.warn("充电桩[{}]枪[{}]状态异常: 故障", pileCode, req.getGunNo());
+            chargingDataService.onGunFault(pileCode, req.getGunNo());
+        }
+    }
+
+    /**
+     * 处理实时监测数据 (0x13)
+     * 这是能耗平台最核心的数据处理
+     */
+    private void handleRealtimeData(ChannelHandlerContext ctx, RealtimeDataFrame data) {
+        log.info("收到实时监测数据: {}", data);
+
+        // 更新心跳时间
+        sessionManager.updateHeartbeat(data.getPileCode());
+
+        // 处理实时数据
+        chargingDataService.processRealtimeData(data);
+    }
+
+    /**
+     * 处理充电握手 (0x15)
+     */
+    private void handleChargingHandshake(ChannelHandlerContext ctx, ChargingHandshakeFrame handshake) {
+        log.info("收到充电握手: {}", handshake);
+
+        // 通知业务层
+        chargingDataService.onChargingHandshake(handshake);
+    }
+
+    /**
+     * 处理充电结束 (0x19)
+     */
+    private void handleChargingEnd(ChannelHandlerContext ctx, ChargingEndFrame end) {
+        log.info("收到充电结束: {}", end);
+
+        // 通知业务层
+        chargingDataService.onChargingEnd(end);
+    }
+
+    /**
+     * 处理错误报文 (0x1B) - 新增
+     * 用于接收充电过程中的通信异常信息
+     */
+    private void handleErrorReport(ChannelHandlerContext ctx, ErrorReportFrame error) {
+        if (error.hasError()) {
+            log.warn("收到错误报文: {}", error);
+        } else {
+            log.debug("收到错误报文(无错误): {}", error);
+        }
+
+        // 通知业务层
+        chargingDataService.onErrorReport(error);
+    }
+
+    /**
+     * 处理远程停机命令回复 (0x35) - 新增
+     */
+    private void handleRemoteStopResp(ChannelHandlerContext ctx, RemoteStopRespFrame resp) {
+        if (resp.isSuccess()) {
+            log.info("远程停机成功: {}", resp);
+        } else {
+            log.warn("远程停机失败: {}", resp);
+        }
+
+        // 通知业务层
+        chargingDataService.onRemoteStopResp(resp);
+    }
+
+    /**
+     * 处理交易记录 (0x3B)
+     * 这是能耗统计最重要的数据,包含分时电量
+     */
+    private void handleTransactionRecord(ChannelHandlerContext ctx, TransactionRecordFrame record) {
+        log.info("收到交易记录: {}", record);
+
+        // 发送确认应答
+        TransactionConfirmFrame confirmFrame = TransactionConfirmFrame.success(
+            record.getTransactionNo(), record.getSequenceNo());
+        ctx.writeAndFlush(confirmFrame);
+
+        log.info("已发送交易记录确认: transNo={}", record.getTransactionNo());
+
+        // 通知业务层处理交易记录
+        chargingDataService.onTransactionRecord(record);
+    }
+
+    /**
+     * 处理工作参数设置应答 (0x51)
+     */
+    private void handleWorkParamSetResp(ChannelHandlerContext ctx, WorkParamSetRespFrame resp) {
+        log.info("收到工作参数设置应答: {}", resp);
+
+        // 通知业务层
+        chargingDataService.onWorkParamSetResp(resp);
+    }
+
+    /**
+     * 验证充电桩
+     * 实际应用中应查询数据库验证桩信息
+     */
+    private boolean validatePile(String pileCode) {
+        // 简单验证:桩编号不为空且长度合法
+        return pileCode != null && !pileCode.isEmpty() && pileCode.length() <= 14;
+    }
+
+    @Override
+    public void channelActive(ChannelHandlerContext ctx) throws Exception {
+        log.info("充电桩连接建立: {}", ctx.channel().remoteAddress());
+        super.channelActive(ctx);
+    }
+
+    /**
+     * 连接断开时的处理
+     * 1. 注销会话
+     * 2. 调用ChargingDataService更新设备离线状态
+     */
+    @Override
+    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+        log.info("充电桩连接断开: {}", ctx.channel().remoteAddress());
+
+        // 1. 先获取桩编码(在注销前获取,否则就拿不到了)
+        String pileCode = sessionManager.getPileCodeByChannel(ctx.channel());
+
+        // 2. 注销会话
+        sessionManager.unregisterSession(ctx.channel());
+
+        // 3. 通知业务层处理设备离线
+        if (pileCode != null) {
+            try {
+                chargingDataService.onPileDisconnect(pileCode);
+            } catch (Exception e) {
+                log.error("处理充电桩断开连接时发生异常 - 桩号: {}", pileCode, e);
+            }
+        }
+
+        super.channelInactive(ctx);
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        log.error("消息处理异常: {} - {}", ctx.channel().remoteAddress(), cause.getMessage(), cause);
+        ctx.close();
+    }
+}

+ 278 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/TaskClacService.java

@@ -0,0 +1,278 @@
+/*
+ * 文 件 名:  TaskService
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/5/22
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common;
+
+import com.ruoyi.ems.common.config.AnalysisConfig;
+import com.ruoyi.ems.domain.Area;
+import com.ruoyi.ems.domain.EmsFacs;
+import com.ruoyi.ems.ba.handle.BaCtlHandler;
+import com.ruoyi.ems.model.QueryMeter;
+import com.ruoyi.ems.service.IAreaService;
+import com.ruoyi.ems.service.IEmsFacsService;
+import com.ruoyi.ems.service.analysis.CarbonCalculationService;
+import com.ruoyi.ems.service.analysis.ElecConsumeForecastService;
+import com.ruoyi.ems.service.analysis.ElecProdForecastService;
+import com.ruoyi.ems.service.analysis.EmsEcoAnalysisService;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import javax.annotation.Resource;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAdjusters;
+import java.util.List;
+
+/**
+ * 调度任务
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/5/22]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Configuration
+public class TaskClacService {
+    private static final Logger log = LoggerFactory.getLogger(TaskClacService.class);
+
+    @Resource
+    private IAreaService areaService;
+
+    @Resource
+    private IEmsFacsService facsService;
+
+
+    @Resource
+    private EmsEcoAnalysisService ecoAnalysisService;
+
+    @Resource
+    private ElecProdForecastService elecProdForecastService;
+
+    @Resource
+    private ElecConsumeForecastService elecConsumeForecastService;
+
+    @Resource
+    private CarbonCalculationService carbonCalculationService;
+
+    @Resource
+    private AnalysisConfig analysisConfig;
+
+    @Qualifier("baCtlHandler")
+    @Resource
+    private BaCtlHandler baCtlHandler;
+
+    /**
+     * 节能分析
+     */
+    @Async("taskExecutor")
+    @Scheduled(cron = "${analysis-task.eco-analysis.cron}")
+    public void triggerEcoAnalysis() {
+        log.debug("start eco analysis task.");
+        DateTimeFormatter dateForm = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+        try {
+            // 获取今天的日期
+            LocalDate today = LocalDate.now();
+            LocalDate yesterday = today.minusDays(1);
+            String date = yesterday.format(dateForm);
+
+            String[] areaCodes = analysisConfig.getEcoAnalysisAreas();
+
+            if (ArrayUtils.isNotEmpty(areaCodes)) {
+                // 获取上个月的日期
+                LocalDate lastMonth = today.minusMonths(1);
+
+                // 获取上个月的第一天
+                LocalDate firstDayOfLastMonth = lastMonth.withDayOfMonth(1);
+
+                // 获取上个月的最后一天
+                LocalDate lastDayOfLastMonth = lastMonth.with(TemporalAdjusters.lastDayOfMonth());
+
+                for (String areaCode : areaCodes) {
+                    QueryMeter queryMeter = new QueryMeter();
+                    queryMeter.setAreaCode(areaCode);
+                    queryMeter.setStartRecTime(firstDayOfLastMonth.format(dateForm));
+                    queryMeter.setEndRecTime(lastDayOfLastMonth.format(dateForm));
+
+                    ecoAnalysisService.executePvForecast(date, queryMeter);
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("eco analysis fail!", e);
+        }
+    }
+
+    /**
+     * 光伏发电预测
+     */
+    @Async("taskExecutor")
+    @Scheduled(cron = "${analysis-task.pv-forecast.cron}")
+    public void pvForecastTask() {
+        log.debug("start pv forecast task.");
+
+        try {
+            // 获取今天的日期
+            LocalDate today = LocalDate.now();
+            String[] areaCodes = analysisConfig.getPvForecastAreas();
+
+            if (ArrayUtils.isNotEmpty(areaCodes)) {
+                String adcode = analysisConfig.getAdcode();
+                int prodForecastDays = analysisConfig.getProdForecastDays();
+
+                for (String areaCode : areaCodes) {
+                    EmsFacs param = new EmsFacs();
+                    param.setRefArea(areaCode);
+                    param.setFacsCategory("E");
+                    List<EmsFacs> facsList = facsService.selectEmsFacsList(param);
+
+                    if (CollectionUtils.isNotEmpty(facsList)) {
+                        for (EmsFacs facs : facsList) {
+                            elecProdForecastService.executePvForecast(adcode, areaCode, facs.getFacsCode(), today,
+                                prodForecastDays);
+                        }
+                    }
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("pv forecast fail!", e);
+        }
+    }
+
+    /**
+     * 用电预测
+     */
+    @Async("taskExecutor")
+    @Scheduled(cron = "${analysis-task.elec-consume-forecast.cron}")
+    public void elecConsumeForecastTask() {
+        log.debug("start pv forecast task.");
+
+        try {
+            // 获取今天的日期
+            LocalDate today = LocalDate.now();
+            String[] areaCodes = analysisConfig.getElecConsumeAreas();
+
+            if (ArrayUtils.isNotEmpty(areaCodes)) {
+                for (String areaCode : areaCodes) {
+                    //  区域用能预测
+                    areaElecConsumeForecast(areaCode, today);
+
+                    //  设施用能预测
+                    facsElecConsumeForecast(areaCode, today);
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("pv forecast fail!", e);
+        }
+    }
+
+    /**
+     * 碳计算(碳排碳汇)
+     */
+    @Async("taskExecutor")
+    @Scheduled(cron = "${analysis-task.carbon-calculation.cron}")
+    public void carbonCalculationTask() {
+        log.debug("start carbon calculation task.");
+
+        try {
+            // 获取今天的日期
+            LocalDate today = LocalDate.now();
+            LocalDate yesterday = today.minusDays(1);
+            String[] areaCodes = analysisConfig.getCarbonCalcAreas();
+
+            if (ArrayUtils.isNotEmpty(areaCodes)) {
+                for (String areaCode : areaCodes) {
+                    // 碳排碳汇
+                    carbonCalculationService.calculateDailyCarbonEmission(areaCode, yesterday);
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("carbon calculation fail!", e);
+        }
+    }
+
+    /**
+     * 碳排预测
+     */
+    @Async("taskExecutor")
+    @Scheduled(cron = "${analysis-task.carbon-forecast.cron}")
+    public void carbonForecastTask() {
+        log.debug("start carbon forecast task.");
+
+        try {
+            // 获取今天的日期
+            LocalDate today = LocalDate.now();
+            String[] areaCodes = analysisConfig.getCarbonForecastAreas();
+
+            if (ArrayUtils.isNotEmpty(areaCodes)) {
+                for (String areaCode : areaCodes) {
+                    // 碳排碳汇
+                    carbonCalculationService.calculateMonthCarbonForecast(areaCode, today);
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("carbon calculation fail!", e);
+        }
+    }
+
+    /**
+     * 每小时产出设备计量数据
+     */
+    @Scheduled(cron = "0 0 0/1 * * ?")
+    public void baMeterHourProd() {
+        int cnt = baCtlHandler.meterHourProd();
+        log.debug("产出室内能耗设备计量数据: {} 条", cnt);
+    }
+
+    private void areaElecConsumeForecast(String rootArea, LocalDate today) {
+        List<Area> areas = areaService.selectAreaTree(rootArea, true);
+        areaElecConsumeForecastSub(rootArea, areas, today);
+    }
+
+    private void areaElecConsumeForecastSub(String rootArea, List<?> areas, LocalDate today) {
+        if (CollectionUtils.isNotEmpty(areas)) {
+            for (Object obj : areas) {
+                Area area = (Area) obj;
+                elecConsumeForecastService.forecastAreas(analysisConfig.getAdcode(), rootArea, area, today,
+                    analysisConfig.getElecConsumeForecastDays());
+
+                if (CollectionUtils.isNotEmpty(area.getChildren())) {
+                    List<?> children = area.getChildren();
+                    areaElecConsumeForecastSub(rootArea, children, today);
+                }
+            }
+        }
+    }
+
+    private void facsElecConsumeForecast(String areaCode, LocalDate today) {
+        EmsFacs param = new EmsFacs();
+        param.setRefArea(areaCode);
+        param.setFacsCategory("Z");
+        List<EmsFacs> facsList = facsService.selectEmsFacsList(param);
+
+        if (CollectionUtils.isNotEmpty(facsList)) {
+            for (EmsFacs facs : facsList) {
+                elecConsumeForecastService.forecastFacilities(analysisConfig.getAdcode(), areaCode, facs, today,
+                    analysisConfig.getElecConsumeForecastDays());
+            }
+        }
+    }
+}

+ 249 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/TaskColExecutor.java

@@ -0,0 +1,249 @@
+/*
+ * 文 件 名:  TaskHandler
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/3/18
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.huashe.common.domain.model.DateAttr;
+import com.huashe.common.domain.model.WeatherForecast;
+import com.huashe.common.domain.model.WeatherRt;
+import com.huashe.common.exception.Assert;
+import com.huashe.common.utils.HttpUtils;
+import com.ruoyi.ems.common.config.DateAttrConfig;
+import com.ruoyi.ems.common.config.WeatherConfig;
+import com.ruoyi.ems.acrel.handle.AcrelElecMonitorHandler;
+import com.ruoyi.ems.ba.handle.BaCtlHandler;
+import com.ruoyi.ems.geekopen.handle.GeekOpenCbHandler;
+import com.ruoyi.ems.growatt.handle.GrowattHandler;
+import com.ruoyi.ems.squarelgt.handle.SquareLightCtlHandler;
+import com.ruoyi.ems.service.IDateService;
+import com.ruoyi.ems.service.IWeatherService;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.client.utils.URIBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.time.LocalDate;
+import java.time.Year;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * 任务调度
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/3/18]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Service
+public class TaskColExecutor {
+    /**
+     * 日志服务
+     */
+    protected static final Logger log = LoggerFactory.getLogger(TaskColExecutor.class);
+
+    @Resource
+    private WeatherConfig weatherConfig;
+
+    @Resource
+    private DateAttrConfig holidayConfig;
+
+    @Resource
+    private IWeatherService weatherService;
+
+    @Resource
+    private IDateService dateService;
+
+    @Qualifier("geekOpenCbHandler")
+    @Resource
+    private GeekOpenCbHandler geekOpenCbHandler;
+
+    @Qualifier("baCtlHandler")
+    @Resource
+    private BaCtlHandler baCtlHandler;
+
+    @Qualifier("squareLightCtlHandler")
+    @Resource
+    private SquareLightCtlHandler squareLightCtlHandler;
+
+    @Qualifier("acrelElecMonitorHandler")
+    @Resource
+    private AcrelElecMonitorHandler acrelElecMonitorHandler;
+
+    @Qualifier("growattHandler")
+    @Resource
+    private GrowattHandler growattHandler;
+
+    /**
+     * 定时刷新设备在线状态
+     */
+    @Scheduled(cron = "0 0/5 * * * ?")
+    public void refresh5min() {
+        CompletableFuture.runAsync(() -> squareLightCtlHandler.execSyncDevAttrAll());
+        CompletableFuture.runAsync(() -> squareLightCtlHandler.refreshOnline());
+        CompletableFuture.runAsync(() -> acrelElecMonitorHandler.execSyncDevAttrAll());
+        CompletableFuture.runAsync(() -> growattHandler.refreshOnline());
+    }
+
+//    /**
+//     * 每小时产出设备计量数据
+//     */
+//    @Scheduled(cron = "0 0 0/1 * * ?")
+//    public void geekOpenMeterHourProd() {
+//        int cnt1 = geekOpenCbHandler.meterHourProd();
+//        log.debug("产出GeekOpen设备计量数据: {} 条", cnt1);
+//    }
+
+    /**
+     * 每15min采集能耗数据
+     */
+    @Scheduled(cron = "0 0/15 * * * ?")
+    public void baMeterCollect() {
+        CompletableFuture.runAsync(() -> {
+            baCtlHandler.meterCollect();
+            baCtlHandler.xfCollect();
+            baCtlHandler.ahuCollect();
+            baCtlHandler.wtCollect();
+            baCtlHandler.wpCollect();
+            baCtlHandler.lightCollect();
+        });
+
+        CompletableFuture.runAsync(() -> growattHandler.execCollectInverterData());
+    }
+
+    /**
+     * 天气实况采集
+     */
+    @Async("taskExecutor")
+    @Scheduled(cron = "${general-data.weather.rt.cron}")
+    public void collectRt() {
+        log.debug("start collect weather rt.");
+
+        try {
+            URIBuilder uriBuilder = new URIBuilder(weatherConfig.getWeatherRtAddr());
+            uriBuilder.addParameter("adcode", weatherConfig.getAdcode())
+                .addParameter("apiKey", weatherConfig.getApiKey());
+            String res = HttpUtils.doGet(uriBuilder);
+            log.debug("get res:\r\n{}", res);
+
+            JSONObject jsonObject = JSON.parseObject(res);
+
+            String code = jsonObject.getString("code");
+            Assert.isTrue(StringUtils.equals(code, "0"), -1, jsonObject.getString("message"));
+
+            String data = jsonObject.getString("data");
+
+            if (data != null) {
+                WeatherRt weatherRt = JSONObject.parseObject(data, WeatherRt.class);
+                weatherService.mergeRt(weatherRt);
+            }
+        }
+        catch (Exception e) {
+            log.error("collectRt fail!", e);
+        }
+    }
+
+    /**
+     * 天气预报采集
+     */
+    @Async("taskExecutor")
+    @Scheduled(cron = "${general-data.weather.forecast.cron}")
+    public void collectForecast() {
+        log.debug("start collect weather forecast.");
+
+        try {
+            URIBuilder uriBuilder = new URIBuilder(weatherConfig.getWeatherForecastAddr());
+            uriBuilder.addParameter("adcode", weatherConfig.getAdcode())
+                .addParameter("apiKey", weatherConfig.getApiKey());
+            String res = HttpUtils.doGet(uriBuilder);
+            log.debug("get res:\r\n{}", res);
+
+            JSONObject jsonObject = JSON.parseObject(res);
+
+            String code = jsonObject.getString("code");
+            Assert.isTrue(StringUtils.equals(code, "0"), -1, jsonObject.getString("message"));
+
+            JSONArray data = jsonObject.getJSONArray("data");
+            Assert.notNull(data, -1, "weather list is null.");
+
+            if (!data.isEmpty()) {
+                List<WeatherForecast> forecastList = JSON.parseArray(data.toString(), WeatherForecast.class);
+
+                if (CollectionUtils.isNotEmpty(forecastList)) {
+                    weatherService.deleteForecastByAdcode(weatherConfig.getAdcode());
+                    weatherService.insertForecastBacth(forecastList);
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("collectForecast fail!", e);
+        }
+    }
+
+    /**
+     * 节假日采集
+     */
+    @Async("taskExecutor")
+    @Scheduled(cron = "${general-data.date.cron}")
+    public void collectDateAttr() {
+        log.debug("start collect date attr.");
+
+        try {
+            // 获取当前年份
+            int currentYear = Year.now().getValue();
+
+            // 本年第一天
+            LocalDate firstDay = LocalDate.of(currentYear, 1, 1);
+
+            // 本年最后一天(12月31日)
+            LocalDate lastDay = LocalDate.of(currentYear, 12, 31);
+
+            // 定义日期格式
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+            URIBuilder uriBuilder = new URIBuilder(holidayConfig.getApiAddr());
+            uriBuilder.addParameter("startDate", formatter.format(firstDay))
+                .addParameter("endDate", formatter.format(lastDay)).addParameter("apiKey", weatherConfig.getApiKey());
+            String res = HttpUtils.doGet(uriBuilder);
+            log.debug("get res:\r\n{}", res);
+
+            JSONObject jsonObject = JSON.parseObject(res);
+
+            String code = jsonObject.getString("code");
+            Assert.isTrue(StringUtils.equals(code, "0"), -1, jsonObject.getString("message"));
+
+            JSONArray data = jsonObject.getJSONArray("data");
+            Assert.notNull(data, -1, "date attr is null.");
+
+            if (!data.isEmpty()) {
+                List<DateAttr> dateAttrs = JSON.parseArray(data.toString(), DateAttr.class);
+
+                if (CollectionUtils.isNotEmpty(dateAttrs)) {
+                    dateService.deleteByYear(String.valueOf(currentYear));
+                    dateService.insertBatch(dateAttrs);
+                }
+            }
+        }
+        catch (Exception e) {
+            log.error("collect date attr fail!", e);
+        }
+    }
+}

+ 83 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/AnalysisConfig.java

@@ -0,0 +1,83 @@
+/*
+ * 文 件 名:  ForecastConfig
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/6/19
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.config;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 预测配置
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/6/19]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Configuration
+public class AnalysisConfig {
+    @Value("${general-data.weather.adcode}")
+    private String adcode;
+
+    @Value("${analysis-task.eco-analysis.area-codes}")
+    private String ecoAnalysisArea;
+
+    @Value("${analysis-task.pv-forecast.area-codes}")
+    private String pvForecastArea;
+
+    @Value("${analysis-task.elec-consume-forecast.area-codes}")
+    private String elecConsumeArea;
+
+    @Value("${analysis-task.carbon-calculation.area-codes}")
+    private String carbonCalcArea;
+
+    @Value("${analysis-task.carbon-forecast.area-codes}")
+    private String carbonForecastArea;
+
+    @Value("${analysis-task.pv-forecast.forecastDays}")
+    private int prodForecastDays;
+
+    @Value("${analysis-task.elec-consume-forecast.forecastDays}")
+    private int elecConsumeForecastDays;
+
+    public String[] getEcoAnalysisAreas() {
+        return StringUtils.split(ecoAnalysisArea, ",");
+    }
+
+    public String[] getPvForecastAreas() {
+        return StringUtils.split(pvForecastArea, ",");
+    }
+
+    public String[] getElecConsumeAreas() {
+        return StringUtils.split(elecConsumeArea, ",");
+    }
+
+    public String[] getCarbonCalcAreas() {
+        return StringUtils.split(carbonCalcArea, ",");
+    }
+
+    public String[] getCarbonForecastAreas() {
+        return StringUtils.split(carbonForecastArea, ",");
+    }
+
+    public int getProdForecastDays() {
+        return prodForecastDays;
+    }
+
+    public int getElecConsumeForecastDays() {
+        return elecConsumeForecastDays;
+    }
+
+    public String getAdcode() {
+        return adcode;
+    }
+}

+ 83 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/AsyncConfig.java

@@ -0,0 +1,83 @@
+/*
+ * 文 件 名:  AsyncConfig
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  异步任务执行器配置
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/26
+ * 修改内容:  配置全局异步任务线程池,解决内存溢出问题
+ */
+package com.ruoyi.ems.common.config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.AsyncConfigurer;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 异步任务执行器配置
+ * 替换Spring默认的SimpleAsyncTaskExecutor,使用线程池管理异步任务
+ * 防止因频繁创建线程导致的内存溢出
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/26]
+ */
+@Configuration
+@EnableAsync
+public class AsyncConfig implements AsyncConfigurer {
+
+    private static final Logger log = LoggerFactory.getLogger(AsyncConfig.class);
+
+    @Value("${async.executor.corePoolSize:4}")
+    private int corePoolSize;
+
+    @Value("${async.executor.maxPoolSize:8}")
+    private int maxPoolSize;
+
+    @Value("${async.executor.queueCapacity:500}")
+    private int queueCapacity;
+
+    @Value("${async.executor.threadNamePrefix:async-task-}")
+    private String threadNamePrefix;
+
+    @Override
+    @Bean(name = "taskExecutor")
+    public Executor getAsyncExecutor() {
+        log.info("初始化全局异步任务线程池 taskExecutor");
+        log.info("配置: corePoolSize={}, maxPoolSize={}, queueCapacity={}, namePrefix={}",
+                corePoolSize, maxPoolSize, queueCapacity, threadNamePrefix);
+
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+
+        executor.setCorePoolSize(corePoolSize);
+        executor.setMaxPoolSize(maxPoolSize);
+        executor.setQueueCapacity(queueCapacity);
+        executor.setThreadNamePrefix(threadNamePrefix);
+
+        executor.setKeepAliveSeconds(60);
+        executor.setAllowCoreThreadTimeOut(true);
+
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+        executor.setAwaitTerminationSeconds(60);
+
+        executor.initialize();
+
+        return executor;
+    }
+
+    @Override
+    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
+        return (throwable, method, params) -> {
+            log.error("异步任务执行异常 - 方法: {}, 异常: {}", method.getName(), throwable.getMessage(), throwable);
+        };
+    }
+}

+ 48 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/DateAttrConfig.java

@@ -0,0 +1,48 @@
+/*
+ * 文 件 名:  Holiday
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/6/9
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 节假日配置
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/6/9]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Configuration
+public class DateAttrConfig {
+    @Value("${general-data.api-key}")
+    private String apiKey;
+
+    @Value("${general-data.date.api-addr}")
+    private String apiAddr;
+
+    public String getApiKey() {
+        return apiKey;
+    }
+
+    public void setApiKey(String apiKey) {
+        this.apiKey = apiKey;
+    }
+
+    public String getApiAddr() {
+        return apiAddr;
+    }
+
+    public void setApiAddr(String apiAddr) {
+        this.apiAddr = apiAddr;
+    }
+}

+ 35 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/EmsConfig.java

@@ -0,0 +1,35 @@
+/*
+ * 文 件 名:  EmsConfig
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/9
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Ems配置
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/9]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "adapter.ems")
+public class EmsConfig {
+    private String url;
+    private int connectTimeout;
+    private int readTimeout;
+    private int writeTimeout;
+    private boolean notifyEnabled;
+}

+ 39 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/MqttConfig.java

@@ -0,0 +1,39 @@
+/*
+ * 文 件 名:  MqttConfig
+ * 版    权:  浩鲸云计算科技股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2024/5/6
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.config;
+
+
+import com.ruoyi.ems.common.mqtt.MqttTemplate;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.annotation.Resource;
+
+/**
+ * Mqtt配置Bean
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2024/5/6]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Configuration
+public class MqttConfig {
+    @Resource
+    private MqttServerProperty mqttProperty;
+
+    @Bean(name = "mqttTemplate")
+    public MqttTemplate getMqttTemplate() throws MqttException {
+        return new MqttTemplate(mqttProperty.getHost(), mqttProperty.getClientId());
+    }
+}

+ 47 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/MqttServerProperty.java

@@ -0,0 +1,47 @@
+/*
+ * 文 件 名:  MqttProperty
+ * 版    权:  浩鲸云计算科技股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2024/4/30
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 连接配置
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2024/4/30]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Configuration
+@ConfigurationProperties(prefix = "mqtt.server")
+public class MqttServerProperty {
+    private String host;
+
+    private String clientId;
+
+    public String getHost() {
+        return host;
+    }
+
+    public void setHost(String host) {
+        this.host = host;
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(String clientId) {
+        this.clientId = clientId;
+    }
+}

+ 69 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/ThreadConfig.java

@@ -0,0 +1,69 @@
+/*
+ * 文 件 名:  Socket
+ * 版    权:  浩鲸云计算科技股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  learshaw
+ * 修改时间:  2020/5/25
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * Scats socket 链路配置
+ * <功能详细描述>
+ *
+ * @author learshaw
+ * @version [版本号, 2021/04/07]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Configuration
+public class ThreadConfig {
+    private static final Logger log = LoggerFactory.getLogger(ThreadConfig.class);
+
+    @Bean(name = "msgHandleExecutor")
+    public ThreadPoolTaskExecutor msgHandleExecutor(@Value("${mqtt.executor.msgHandle.corePoolSize}") int corePoolSize,
+        @Value("${mqtt.executor.msgHandle.maxPoolSize}") int maxPoolSize,
+        @Value("${mqtt.executor.msgHandle.queueCapacity}") int queueCapacity,
+        @Value("${mqtt.executor.msgHandle.namePrefix}") String namePrefix) {
+        log.info("start msgHandleExecutor");
+        return buildExecutor(corePoolSize, maxPoolSize, queueCapacity, namePrefix,
+            new ThreadPoolExecutor.DiscardOldestPolicy());
+    }
+
+    private ThreadPoolTaskExecutor buildExecutor(int corePoolSize, int maxPoolSize, int queueCapacity,
+        String namePrefix, RejectedExecutionHandler rejectedExecutionHandler) {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        //配置核心线程数
+        executor.setCorePoolSize(corePoolSize);
+        //配置最大线程数
+        executor.setMaxPoolSize(maxPoolSize);
+        //配置队列大小
+        executor.setQueueCapacity(queueCapacity);
+        //配置线程池中的线程的名称前缀
+        executor.setThreadNamePrefix(namePrefix);
+
+        // 线程池对拒绝任务(无限程可用)的处理策略
+        // ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
+        // ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
+        // ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
+        // ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务,如果执行器已关闭,则丢弃.
+        executor.setRejectedExecutionHandler(rejectedExecutionHandler);
+        //执行初始化
+        executor.initialize();
+
+        return executor;
+    }
+}

+ 70 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/config/WeatherConfig.java

@@ -0,0 +1,70 @@
+/*
+ * 文 件 名:  WeatherConfig
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/5/27
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 天气采集配
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/5/27]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Configuration
+public class WeatherConfig {
+    @Value("${general-data.api-key}")
+    private String apiKey;
+
+    @Value("${general-data.weather.adcode}")
+    private String adcode;
+
+    @Value("${general-data.weather.rt.api-addr}")
+    private String weatherRtAddr;
+
+    @Value("${general-data.weather.forecast.api-addr}")
+    private String weatherForecastAddr;
+
+    public String getAdcode() {
+        return adcode;
+    }
+
+    public void setAdcode(String adcode) {
+        this.adcode = adcode;
+    }
+
+    public String getApiKey() {
+        return apiKey;
+    }
+
+    public void setApiKey(String apiKey) {
+        this.apiKey = apiKey;
+    }
+
+    public String getWeatherRtAddr() {
+        return weatherRtAddr;
+    }
+
+    public void setWeatherRtAddr(String weatherRtAddr) {
+        this.weatherRtAddr = weatherRtAddr;
+    }
+
+    public String getWeatherForecastAddr() {
+        return weatherForecastAddr;
+    }
+
+    public void setWeatherForecastAddr(String weatherForecastAddr) {
+        this.weatherForecastAddr = weatherForecastAddr;
+    }
+}

+ 234 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/handle/BaseDevHandler.java

@@ -0,0 +1,234 @@
+/*
+ * 文 件 名:  BaseDevHandler
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/4/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.handle;
+
+import com.huashe.common.utils.DateUtils;
+import com.ruoyi.common.redis.service.RedisService;
+import com.ruoyi.ems.domain.EmsDevice;
+import com.ruoyi.ems.domain.EmsObjAbilityCallLog;
+import com.ruoyi.ems.domain.EmsObjAttrValue;
+import com.ruoyi.ems.domain.EmsObjEventLog;
+import com.ruoyi.ems.enums.DevOnlineStatus;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import com.ruoyi.ems.geekopen.MqttCacheMsg;
+import com.ruoyi.ems.service.IElecMeterHService;
+import com.ruoyi.ems.service.IElecPgSupplyHService;
+import com.ruoyi.ems.service.IEmsDeviceService;
+import com.ruoyi.ems.service.IEmsFacsService;
+import com.ruoyi.ems.service.IEmsObjAbilityCallLogService;
+import com.ruoyi.ems.service.IEmsObjAttrValueService;
+import com.ruoyi.ems.service.IEmsObjEventLogService;
+import com.ruoyi.ems.service.IFdEnergyPriceConfigService;
+import com.ruoyi.ems.service.IPriceService;
+import com.ruoyi.ems.service.IWaterMeterHService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 设备处理基类
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/4/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Slf4j
+public abstract class BaseDevHandler {
+    @Autowired
+    protected RedisService redisService;
+
+    @Autowired
+    protected IEmsFacsService facsService;
+
+    @Autowired
+    protected IEmsDeviceService deviceService;
+
+    @Autowired
+    protected IEmsObjAttrValueService objAttrValueService;
+
+    @Autowired
+    protected IElecMeterHService elecMeterHService;
+
+    @Autowired
+    protected IWaterMeterHService waterMeterHService;
+
+    @Autowired
+    protected IPriceService priceService;
+
+    @Autowired
+    protected IFdEnergyPriceConfigService fdEnergyPriceConfigService;
+
+    @Autowired
+    protected IEmsObjAbilityCallLogService objAbilityCallLogService;
+
+    @Autowired
+    protected IEmsObjEventLogService objEventLogService;
+
+    @Autowired
+    protected IElecPgSupplyHService pgSupplyHService;
+
+    /**
+     * 抽象方法,用于能力调用
+     *
+     * @param abilityParam 能力参数
+     * @return 处理结果字符串
+     */
+    public abstract CallResponse<Void> call(AbilityPayload abilityParam);
+
+    /**
+     * 获取设备列表
+     *
+     * @return 设备列表
+     */
+    public abstract List<EmsDevice> getDeviceList();
+
+    /**
+     * 最后一次消息时间缓存key
+     */
+    protected static final String MQTT_LAST_TIME = "mqttLastMsgTime";
+
+    /**
+     * 更新最后消息时
+     *
+     * @param device    设备
+     * @param timeValue 时间值
+     */
+    public void updateMsgTime(EmsDevice device, Date timeValue) {
+        try {
+            String dateTime = DateUtils.dateToString(timeValue, DateUtils.YYYY_MM_DD_HH_MM_SS);
+
+            // 更新缓存
+            updateCacheAfterSuccess(device.getDeviceCode(), MQTT_LAST_TIME, dateTime);
+        }
+        catch (Exception e) {
+            log.error("刷新缓存消息时间失败!", e);
+        }
+    }
+
+    /**
+     * 更新缓存值
+     *
+     * @param deviceCode 设备信息
+     * @param attrKey    属性值
+     * @param newValue
+     */
+    public void updateCacheAfterSuccess(String deviceCode, String attrKey, String newValue) {
+        redisService.setCacheMapValue(deviceCode, attrKey, newValue);
+    }
+
+    /**
+     * 刷新设备状态
+     *
+     * @param device 设备信息
+     */
+    public void refreshStatus(EmsDevice device, DevOnlineStatus status) {
+        if (null == device.getDeviceStatus() || device.getDeviceStatus() != status.getCode()) {
+            device.setDeviceStatus(status.getCode());
+            deviceService.updateEmsDevice(device);
+            triggerEvent(device, status == DevOnlineStatus.ONLINE ? "online" : "offline", null, new Date());
+            log.debug("设备[{}]{}...", device.getDeviceCode(), status == DevOnlineStatus.ONLINE ? "上线" : "下线");
+        }
+    }
+
+    /**
+     * 触发设备事件
+     *
+     * @param device   设备信息
+     * @param eventKey 事件标记
+     * @param detail   事件详情
+     * @param date     时间戳
+     */
+    public void triggerEvent(EmsDevice device, String eventKey, String detail, Date date) {
+        EmsObjEventLog eventLog = new EmsObjEventLog();
+        eventLog.setObjCode(device.getDeviceCode());
+        eventLog.setModelCode(device.getDeviceModel());
+        eventLog.setEventKey(eventKey);
+        eventLog.setEventTime(date);
+        eventLog.setEventDetail(detail);
+        objEventLogService.insertLog(eventLog);
+    }
+
+    /**
+     * 触发设备事件
+     *
+     * @param modelCode 模型代码
+     * @param objCode   对象编码
+     * @param detail    事件详情
+     * @param date      时间戳
+     */
+    public void triggerEvent(String modelCode, String objCode, String eventKey, String detail, Date date) {
+        EmsObjEventLog eventLog = new EmsObjEventLog();
+        eventLog.setModelCode(modelCode);
+        eventLog.setObjCode(objCode);
+        eventLog.setEventKey(eventKey);
+        eventLog.setEventTime(date);
+        eventLog.setEventDetail(detail);
+        objEventLogService.insertLog(eventLog);
+    }
+
+    protected EmsObjAbilityCallLog saveCallLog(AbilityPayload abilityParam, String callPayload, long sendTime, int callStatus) {
+        EmsObjAbilityCallLog objAbilityCallLog = new EmsObjAbilityCallLog();
+        objAbilityCallLog.setObjCode(abilityParam.getObjCode());
+        objAbilityCallLog.setModelCode(abilityParam.getModelCode());
+        objAbilityCallLog.setAbilityKey(abilityParam.getAbilityKey());
+        objAbilityCallLog.setCallTime(new Date(sendTime));
+        objAbilityCallLog.setCallPayload(callPayload);
+        objAbilityCallLog.setCallStatus(callStatus);
+        objAbilityCallLogService.addLog(objAbilityCallLog);
+        return objAbilityCallLog;
+    }
+
+    protected void updateCallLog(EmsObjAbilityCallLog objAbilityCallLog, MqttCacheMsg cacheMsg, int callStatus) {
+        objAbilityCallLog.setResTime(cacheMsg.getReceiveTime());
+        objAbilityCallLog.setResPayload(cacheMsg.getPayload());
+        objAbilityCallLog.setCallStatus(callStatus);
+        objAbilityCallLogService.updateLog(objAbilityCallLog);
+    }
+
+    protected EmsObjAbilityCallLog saveCallLog(String objCode, String modelCode, String abilityKey, Integer callStatus,
+        String callPayload, String resPayload) {
+        EmsObjAbilityCallLog objAbilityCallLog = new EmsObjAbilityCallLog();
+        objAbilityCallLog.setObjCode(objCode);
+        objAbilityCallLog.setModelCode(modelCode);
+        objAbilityCallLog.setAbilityKey(abilityKey);
+        objAbilityCallLog.setCallTime(new Date());
+        objAbilityCallLog.setCallPayload(callPayload);
+        objAbilityCallLog.setCallStatus(callStatus);
+        objAbilityCallLog.setResTime(new Date());
+        objAbilityCallLog.setResPayload(resPayload);
+        objAbilityCallLogService.addLog(objAbilityCallLog);
+        return objAbilityCallLog;
+    }
+
+    public abstract void refreshOnline();
+
+    protected void checkAndUpdate(Map<String, EmsObjAttrValue> objAttrValueMap, String objCode, String modelCode,
+        String key, String newValue) {
+        EmsObjAttrValue objAttrValue = objAttrValueMap.get(key);
+
+        if (null != objAttrValue) {
+            if (!StringUtils.equals(objAttrValue.getAttrValue(), newValue)) {
+                objAttrValueService.updateObjAttrValue(modelCode, objCode, key, newValue);
+            }
+        }
+        else {
+            objAttrValue = new EmsObjAttrValue(objCode, modelCode, key, newValue);
+            objAttrValueService.mergeObjAttrValue(objAttrValue);
+        }
+    }
+}

+ 396 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/handle/BaseMeterDevHandler.java

@@ -0,0 +1,396 @@
+/*
+ * 文 件 名:  MeterDevHandler
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/4/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.handle;
+
+import com.huashe.common.utils.DateUtils;
+import com.ruoyi.ems.domain.ElecMeterH;
+import com.ruoyi.ems.domain.FdEnergyPriceConfig;
+import com.ruoyi.ems.domain.MeterDevice;
+import com.ruoyi.ems.domain.WaterMeterH;
+import com.ruoyi.ems.model.Price;
+import com.ruoyi.ems.service.IMeterDeviceService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 计量设备处理类
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/4/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Slf4j
+public abstract class BaseMeterDevHandler extends BaseDevHandler {
+    @Autowired
+    protected IMeterDeviceService meterDeviceService;
+
+    protected static final String LAST_HOUR_READING = "LAST_HOUR_READING";
+
+    protected static final String NEW_HOUR_READING = "NEW_HOUR_READING";
+
+    protected static final String LAST_HOUR_TIMESTAMP = "LAST_HOUR_TIMESTAMP";
+
+    protected static final long MAX_CACHE_AGE_MILLIS = 2 * 60 * 60 * 1000;
+
+
+    /**
+     * 获取计量设备列表
+     *
+     * @return 设备列表
+     */
+    public abstract List<MeterDevice> getMeterDeviceList();
+
+    /**
+     * 产出小时电量计量
+     */
+    public int meterHourProd() {
+        int cnt = 0;
+
+        try {
+            List<MeterDevice> deviceList = getMeterDeviceList();
+            List<ElecMeterH> meterHList = new ArrayList<>();
+            Map<String, Price> priceMap = new ConcurrentHashMap<>();
+            Date date = DateUtils.adjustHour(new Date(), -1);
+
+            for (MeterDevice device : deviceList) {
+                // 读取历史抄表
+                String lastMeterReading = redisService.getCacheMapValue(device.getDeviceCode(), LAST_HOUR_READING);
+
+                // 读取最新抄表值
+                String newMeterReading = redisService.getCacheMapValue(device.getDeviceCode(), NEW_HOUR_READING);
+
+                ElecMeterH elecMeterH = getElecMeterH(device, lastMeterReading, newMeterReading);
+
+                if (null != elecMeterH) {
+                    Price price = priceMap.computeIfAbsent(device.getAreaCode(),
+                        k -> priceService.getElecHourPrice(device.getAreaCode(), date));
+                    completeElecPrice(elecMeterH, price);
+                    meterHList.add(elecMeterH);
+                }
+            }
+
+            if (CollectionUtils.isNotEmpty(meterHList)) {
+                cnt = elecMeterHService.insertBatch(meterHList);
+            }
+        }
+        catch (Exception e) {
+            log.error("定时产出小时电量计量失败!", e);
+        }
+
+        return cnt;
+    }
+
+    public ElecMeterH getElecMeterH(MeterDevice mDevice, String lastMeterReading, String newMeterReading) {
+        return getElecMeterH(mDevice.getDeviceCode(), mDevice, lastMeterReading, newMeterReading);
+    }
+
+    public ElecMeterH getElecMeterH(String deviceCode, MeterDevice mDevice, String lastMeterReading,
+        String newMeterReading) {
+        ElecMeterH elecMeterH = null;
+
+        // 缓存不为空,使用缓存抄表计算
+        if (StringUtils.isNotEmpty(lastMeterReading)) {
+            elecMeterH = execElecHourMeter(mDevice, lastMeterReading, newMeterReading);
+            // 本周期抄表完成,将新抄表值覆盖作为下个周期的起始
+            updateCacheAfterSuccess(deviceCode, LAST_HOUR_READING, newMeterReading);
+        }
+        // 缓存为空,使用数据库记录计算
+        else {
+            ElecMeterH dbElecMeterH = elecMeterHService.selectLatelyItem(deviceCode);
+
+            if (null != dbElecMeterH && null != dbElecMeterH.getMeterReading()) {
+                elecMeterH = execElecHourMeter(mDevice, String.valueOf(dbElecMeterH.getMeterReading()),
+                    newMeterReading);
+                // 本周期抄表完成,将新抄表值覆盖作为下个周期的起始
+                updateCacheAfterSuccess(deviceCode, LAST_HOUR_READING, newMeterReading);
+            }
+            else {
+                // 无缓存也无数据库记录,则认为是首次上报
+                if (StringUtils.isNotEmpty(newMeterReading)) {
+                    updateCacheAfterSuccess(deviceCode, LAST_HOUR_READING, newMeterReading);
+                }
+            }
+        }
+
+        return elecMeterH;
+    }
+
+    /**
+     * 执行小时抄表计算
+     *
+     * @param mDevice          计量设备
+     * @param lastMeterReading 上次抄表值
+     * @param newMeterReading  本次抄表值
+     * @return
+     */
+    protected ElecMeterH execElecHourMeter(MeterDevice mDevice, String lastMeterReading, String newMeterReading) {
+        ElecMeterH elecMeterH = null;
+
+        if (StringUtils.isEmpty(newMeterReading) || StringUtils.isEmpty(lastMeterReading)) {
+            log.warn("电表读数为空 - 设备:{}, 新值:{}, 旧值:{}",
+                mDevice.getDeviceCode(), newMeterReading, lastMeterReading);
+            return null;
+        }
+
+        try {
+            BigDecimal lastEngValue = new BigDecimal(lastMeterReading);
+            BigDecimal newEngValue = new BigDecimal(newMeterReading);
+
+            // 计算电量差值
+            BigDecimal useQuantity = newEngValue.subtract(lastEngValue);
+
+            // 数据合理性校验
+            if (useQuantity.doubleValue() < 0) {
+                // 处理表计重置的情况
+                if (newEngValue.doubleValue() < 100 && lastEngValue.doubleValue() > 10000) {
+                    log.info("检测到电表可能重置 - 设备:{}, 新值:{}, 旧值:{}",
+                        mDevice.getDeviceCode(), newEngValue, lastEngValue);
+                    // 表计重置时,本次不产生数据
+                    return null;
+                } else {
+                    log.error("检测到电表异常负值 - 设备:{}, 新值:{}, 旧值:{}, 差值:{}",
+                        mDevice.getDeviceCode(), newEngValue, lastEngValue, useQuantity);
+                    return null;
+                }
+            }
+
+            // 检查是否增长过快(每小时最大1000度)
+            if (useQuantity.doubleValue() > 1000.0) {
+                log.warn("电表用量增长异常 - 设备:{}, 小时用量:{}度, 新值:{}, 旧值:{}",
+                    mDevice.getDeviceCode(), useQuantity, newEngValue, lastEngValue);
+            }
+
+            // 倍率计算
+            useQuantity = magnification(useQuantity, mDevice.getMagnification());
+            // 封装计量对象
+            elecMeterH = buildElecMeterH(mDevice, newEngValue, useQuantity);
+
+        } catch (NumberFormatException e) {
+            log.error("电表读数格式错误 - 设备:{}, 新值:{}, 旧值:{}",
+                mDevice.getDeviceCode(), newMeterReading, lastMeterReading, e);
+        }
+
+        return elecMeterH;
+    }
+
+    /**
+     * 封装计量对象
+     *
+     * @param device      设备信息
+     * @param newEngValue 最新电量值
+     * @param useQuantity 用量差值
+     * @return ElecMeterH 计量对象
+     */
+    private ElecMeterH buildElecMeterH(MeterDevice device, BigDecimal newEngValue, BigDecimal useQuantity) {
+        Date date = DateUtils.adjustHour(new Date(), -1);
+        ElecMeterH elecMeterH = new ElecMeterH();
+        elecMeterH.setAreaCode(device.getAreaCode());
+        elecMeterH.setDeviceCode(device.getDeviceCode());
+        elecMeterH.setRecordTime(date);
+        elecMeterH.setDate(date);
+        elecMeterH.setTime(date);
+        elecMeterH.setTimeIndex(getHourIndex(date));
+        elecMeterH.setElecQuantity(useQuantity.doubleValue());
+        elecMeterH.setMeterReading(newEngValue.doubleValue());
+
+        return elecMeterH;
+    }
+
+    public WaterMeterH getWaterMeterH(MeterDevice mDevice, String lastMeterReading, String newMeterReading) {
+        return getWaterMeterH(mDevice.getDeviceCode(), mDevice, lastMeterReading, newMeterReading);
+    }
+
+    public WaterMeterH getWaterMeterH(String deviceCode, MeterDevice mDevice, String lastMeterReading,
+        String newMeterReading) {
+        WaterMeterH waterMeterH = null;
+
+        // 缓存不为空,使用缓存抄表计算
+        if (StringUtils.isNotEmpty(lastMeterReading)) {
+            waterMeterH = execWaterHourMeter(mDevice, lastMeterReading, newMeterReading);
+
+            // 更新缓存
+            updateCacheAfterSuccess(deviceCode, LAST_HOUR_READING, newMeterReading);
+        }
+        // 缓存为空,使用数据库记录计算
+        else {
+            WaterMeterH dbWaterMeterH = waterMeterHService.selectLatelyItem(mDevice.getDeviceCode());
+
+            if (null != dbWaterMeterH && null != dbWaterMeterH.getMeterReading()) {
+                waterMeterH = execWaterHourMeter(mDevice, String.valueOf(dbWaterMeterH.getMeterReading()),
+                    newMeterReading);
+
+                // 更新缓存
+                updateCacheAfterSuccess(deviceCode, LAST_HOUR_READING, newMeterReading);
+            }
+            else {
+                // 无缓存也无数据库记录,则认为是首次上报
+                if (StringUtils.isNotEmpty(newMeterReading)) {
+                    updateCacheAfterSuccess(deviceCode, LAST_HOUR_READING, newMeterReading);
+                }
+            }
+        }
+
+        return waterMeterH;
+    }
+
+    /**
+     * 执行小时抄表计算
+     *
+     * @param device           设备信息
+     * @param lastMeterReading 上次抄表值
+     * @param newMeterReading  本次抄表值
+     * @return
+     */
+    protected WaterMeterH execWaterHourMeter(MeterDevice device, String lastMeterReading, String newMeterReading) {
+        WaterMeterH waterMeterH = null;
+
+        if (StringUtils.isEmpty(newMeterReading) || StringUtils.isEmpty(lastMeterReading)) {
+            log.warn("水表读数为空 - 设备:{}, 新值:{}, 旧值:{}",
+                device.getDeviceCode(), newMeterReading, lastMeterReading);
+            return null;
+        }
+
+        try {
+            BigDecimal lastEngValue = new BigDecimal(lastMeterReading);
+            BigDecimal newEngValue = new BigDecimal(newMeterReading);
+
+            // 计算水量差值
+            BigDecimal useQuantity = newEngValue.subtract(lastEngValue);
+
+            // 数据合理性校验
+            if (useQuantity.doubleValue() < 0) {
+                // 处理表计重置的情况
+                if (newEngValue.doubleValue() < 10 && lastEngValue.doubleValue() > 1000) {
+                    log.info("检测到水表可能重置 - 设备:{}, 新值:{}, 旧值:{}",
+                        device.getDeviceCode(), newEngValue, lastEngValue);
+                    // 表计重置时,本次不产生数据
+                    return null;
+                } else {
+                    log.error("检测到水表异常负值 - 设备:{}, 新值:{}, 旧值:{}, 差值:{}",
+                        device.getDeviceCode(), newEngValue, lastEngValue, useQuantity);
+                    return null;
+                }
+            }
+
+            // 检查是否增长过快(每小时最大100吨)
+            if (useQuantity.doubleValue() > 100.0) {
+                log.warn("水表用量增长异常 - 设备:{}, 小时用量:{}吨, 新值:{}, 旧值:{}",
+                    device.getDeviceCode(), useQuantity, newEngValue, lastEngValue);
+            }
+
+            // 倍率计算
+            useQuantity = magnification(useQuantity, device.getMagnification());
+            // 封装计量对象
+            waterMeterH = buildWaterMeterH(device, newEngValue, useQuantity);
+
+        } catch (NumberFormatException e) {
+            log.error("水表读数格式错误 - 设备:{}, 新值:{}, 旧值:{}",
+                device.getDeviceCode(), newMeterReading, lastMeterReading, e);
+        }
+
+        return waterMeterH;
+    }
+
+    /**
+     * 封装计量对象
+     *
+     * @param device      设备信息
+     * @param newEngValue 最新电量值
+     * @param useQuantity 用量差值
+     * @return ElecMeterH 计量对象
+     */
+    private WaterMeterH buildWaterMeterH(MeterDevice device, BigDecimal newEngValue, BigDecimal useQuantity) {
+        Date date = DateUtils.adjustHour(new Date(), -1);
+        WaterMeterH waterMeterH = new WaterMeterH();
+        waterMeterH.setAreaCode(device.getAreaCode());
+        waterMeterH.setDeviceCode(device.getDeviceCode());
+        waterMeterH.setRecordTime(date);
+        waterMeterH.setDate(date);
+        waterMeterH.setTime(date);
+        waterMeterH.setTimeIndex(getHourIndex(date));
+        waterMeterH.setWaterQuantity(useQuantity.doubleValue());
+        waterMeterH.setMeterReading(newEngValue.doubleValue());
+
+        return waterMeterH;
+    }
+
+    /**
+     * 倍率计算
+     *
+     * @param quantity      原始用量
+     * @param magnification 倍率
+     * @return
+     */
+    private BigDecimal magnification(BigDecimal quantity, Integer magnification) {
+        return null != magnification ? quantity.multiply(new BigDecimal(String.valueOf(magnification))) : quantity;
+    }
+
+    /**
+     * 完善价格信息
+     *
+     * @param elecMeterH 计量对象
+     * @param price      价格信息
+     */
+    public void completeElecPrice(ElecMeterH elecMeterH, Price price) {
+        if (null != price) {
+            BigDecimal useQuantity = new BigDecimal(String.valueOf(elecMeterH.getElecQuantity()));
+            BigDecimal cost = useQuantity.multiply(new BigDecimal(String.valueOf(price.getPriceValue())));
+            elecMeterH.setMeterType(price.getMeterType());
+            elecMeterH.setMeterUnitPrice(price.getPriceValue());
+            elecMeterH.setUseElecCost(cost.doubleValue());
+        }
+    }
+
+    /**
+     * 完善价格信息
+     *
+     * @param waterMeterH 计量对象
+     * @param config      价格信息
+     */
+    public void completeWaterPrice(WaterMeterH waterMeterH, FdEnergyPriceConfig config) {
+        if (null != config) {
+            BigDecimal useQuantity = new BigDecimal(String.valueOf(waterMeterH.getWaterQuantity()));
+            BigDecimal cost = useQuantity.multiply(new BigDecimal(String.valueOf(config.getUnitPrice())));
+            waterMeterH.setMeterUnitPrice(config.getUnitPrice());
+            waterMeterH.setUseWaterCost(cost.doubleValue());
+        }
+    }
+
+    protected static int getHourIndex(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        int hour = calendar.get(Calendar.HOUR_OF_DAY);
+        return hour + 1;
+    }
+
+    /**
+     * 格式化数值为指定小数位数
+     *
+     * @param value 原始值
+     * @param decimalPlaces 小数位数
+     * @return 格式化后的值
+     */
+    protected double formatToDecimalPlaces(double value, int decimalPlaces) {
+        return Math.round(value * Math.pow(10, decimalPlaces)) / Math.pow(10, decimalPlaces);
+    }
+}

+ 85 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/handle/RootMsgHandler.java

@@ -0,0 +1,85 @@
+/*
+ * 文 件 名:  MessageHandler
+ * 版    权:  浩鲸云计算科技股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2024/4/30
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.handle;
+
+import com.ruoyi.ems.common.mqtt.MqttMessageHandler;
+import com.ruoyi.ems.geekopen.handle.GeekOpenCbHandler;
+import com.ruoyi.ems.keka.handle.Keka86BsHandler;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 消息处理handle
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2024/4/30]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Service
+public class RootMsgHandler implements MqttMessageHandler {
+    private static final Logger log = LoggerFactory.getLogger(RootMsgHandler.class);
+
+    @Autowired
+    private GeekOpenCbHandler geekOpenCbHandler;
+
+    @Autowired
+    private Keka86BsHandler keka86BsHandler;
+
+    @Override
+    public void handle(String topic, byte[] message) {
+        try {
+            if (StringUtils.startsWith(topic, "/server/dlq/")) {
+                String payload = convertToString(message);
+                log.info("[Receive] Topic:{}, message:{}", topic, payload);
+
+                String deviceCode = StringUtils.substringAfter(topic, "/server/dlq/");
+                geekOpenCbHandler.msgHandle(deviceCode, payload);
+            }
+            else if (StringUtils.startsWith(topic, "/sc/dtu/rep/")) {
+                String payload = convertToHexString(message);
+                log.info("[Receive] Topic:{}, message:{}", topic, payload);
+                String deviceCode = StringUtils.substringAfter(topic, "/sc/dtu/rep/");
+                keka86BsHandler.msgHandle(deviceCode, payload);
+            }
+        }
+        catch (Exception e) {
+            log.error("[Handle]Topic:{}, message:{}", topic, message, e);
+        }
+    }
+
+    /**
+     * 将payload转换为字符串 (用于文本类消息)
+     */
+    private String convertToString(byte[] message) {
+        return new String(message, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * 将payload转换为十六进制字符串 (用于Modbus二进制消息)
+     */
+    private String convertToHexString(byte[] message) {
+        StringBuilder sb = new StringBuilder();
+
+        for (int i = 0; i < message.length; i++) {
+            if (i > 0)
+                sb.append(" ");
+            sb.append(String.format("%02X", message[i] & 0xFF));
+        }
+        return sb.toString();
+    }
+}

+ 66 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mock/MockGccController.java

@@ -0,0 +1,66 @@
+/*
+ * 文 件 名:  MockGccController
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2026/2/4
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.mock;
+
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Mock 光储充
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2026/2/4]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@RestController
+@CrossOrigin(allowedHeaders = "*", allowCredentials = "false")
+@RequestMapping("/gcc")
+@Api(value = "MockGccController", description = "光储冲柔控制Api")
+public class MockGccController {
+    private static final Logger log = LoggerFactory.getLogger(MockGccController.class);
+
+    /**
+     * 采集器能力调用
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/ct/abilityCall", method = RequestMethod.POST)
+    @ApiOperation(value = "/ct/abilityCall", notes = "采集器能力调用")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public CallResponse<Void> call(@RequestBody AbilityPayload abilityPayload) {
+        CallResponse<Void> res = null;
+
+        try {
+            res = new CallResponse<>(0, "成功");
+        }
+        catch (Exception e) {
+            log.error("call fail!", e);
+            res = new CallResponse<>(501, "内部错误:" + e.getMessage());
+        }
+
+        return res;
+    }
+}

+ 66 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mock/MockGczrController.java

@@ -0,0 +1,66 @@
+/*
+ * 文 件 名:  MockGczrController
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2026/2/4
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.mock;
+
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Mock 光储直柔
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2026/2/4]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@RestController
+@CrossOrigin(allowedHeaders = "*", allowCredentials = "false")
+@RequestMapping("/gczr")
+@Api(value = "MockGczrController", description = "光储直柔控制Api")
+public class MockGczrController {
+    private static final Logger log = LoggerFactory.getLogger(MockGczrController.class);
+
+    /**
+     * 采集器能力调用
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/ct/abilityCall", method = RequestMethod.POST)
+    @ApiOperation(value = "/ct/abilityCall", notes = "采集器能力调用")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public CallResponse<Void> call(@RequestBody AbilityPayload abilityPayload) {
+        CallResponse<Void> res = null;
+
+        try {
+            res = new CallResponse<>(0, "成功");
+        }
+        catch (Exception e) {
+            log.error("call fail!", e);
+            res = new CallResponse<>(501, "内部错误:" + e.getMessage());
+        }
+
+        return res;
+    }
+}

+ 66 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mock/MockHmController.java

@@ -0,0 +1,66 @@
+/*
+ * 文 件 名:  MockGccController
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2026/2/4
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.mock;
+
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Mock 海绵
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2026/2/4]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@RestController
+@CrossOrigin(allowedHeaders = "*", allowCredentials = "false")
+@RequestMapping("/hm")
+@Api(value = "MockHmController", description = "海绵控制Api")
+public class MockHmController {
+    private static final Logger log = LoggerFactory.getLogger(MockHmController.class);
+
+    /**
+     * 采集器能力调用
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/ct/abilityCall", method = RequestMethod.POST)
+    @ApiOperation(value = "/ct/abilityCall", notes = "采集器能力调用")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public CallResponse<Void> call(@RequestBody AbilityPayload abilityPayload) {
+        CallResponse<Void> res = null;
+
+        try {
+            res = new CallResponse<>(0, "成功");
+        }
+        catch (Exception e) {
+            log.error("call fail!", e);
+            res = new CallResponse<>(501, "内部错误:" + e.getMessage());
+        }
+
+        return res;
+    }
+}

+ 66 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mock/MockLjcyController.java

@@ -0,0 +1,66 @@
+/*
+ * 文 件 名:  MockGccController
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2026/2/4
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.mock;
+
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Mock 垃圾厨余
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2026/2/4]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@RestController
+@CrossOrigin(allowedHeaders = "*", allowCredentials = "false")
+@RequestMapping("/ljcy")
+@Api(value = "MockLjcyController", description = "垃圾厨余控制Api")
+public class MockLjcyController {
+    private static final Logger log = LoggerFactory.getLogger(MockLjcyController.class);
+
+    /**
+     * 采集器能力调用
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/ct/abilityCall", method = RequestMethod.POST)
+    @ApiOperation(value = "/ct/abilityCall", notes = "采集器能力调用")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public CallResponse<Void> call(@RequestBody AbilityPayload abilityPayload) {
+        CallResponse<Void> res = null;
+
+        try {
+            res = new CallResponse<>(0, "成功");
+        }
+        catch (Exception e) {
+            log.error("call fail!", e);
+            res = new CallResponse<>(501, "内部错误:" + e.getMessage());
+        }
+
+        return res;
+    }
+}

+ 216 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mock/MockPileController.java

@@ -0,0 +1,216 @@
+/*
+ * 文 件 名:  MockPileController
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  Mock充电桩控制接口
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/23
+ */
+package com.ruoyi.ems.common.mock;
+
+import com.ruoyi.ems.charging.mock.model.MockGunState;
+import com.ruoyi.ems.charging.mock.simulator.MockPileClient;
+import com.ruoyi.ems.charging.mock.simulator.MockPileClientManager;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Mock充电桩控制接口
+ * 提供REST API用于手动控制Mock客户端
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/23]
+ */
+@RestController
+@RequestMapping("/api/mock/charging")
+@ConditionalOnProperty(name = "adapter.charging-pile.mock.enabled", havingValue = "true")
+public class MockPileController {
+
+    @Autowired
+    private MockPileClientManager clientManager;
+
+    /**
+     * 启动mock
+     */
+    @GetMapping("/startClient")
+    public String startClient() {
+        clientManager.startAll();
+        return "success";
+    }
+
+    /**
+     * 停止mock
+     */
+    @GetMapping("/stopClient")
+    public String stopClient() {
+        clientManager.stopAll();
+        return "success";
+    }
+
+    /**
+     * 获取状态统计
+     */
+    @GetMapping("/status")
+    public Map<String, Object> getStatus() {
+        return clientManager.getStatistics();
+    }
+
+    /**
+     * 获取所有客户端状态
+     */
+    @GetMapping("/clients")
+    public List<Map<String, Object>> getClients() {
+        List<Map<String, Object>> result = new ArrayList<>();
+
+        for (MockPileClient client : clientManager.getAllClients()) {
+            Map<String, Object> clientInfo = new LinkedHashMap<>();
+            clientInfo.put("pileCode", client.getPileCode());
+            clientInfo.put("connected", client.isConnected());
+            clientInfo.put("loggedIn", client.isLoggedIn());
+
+            List<Map<String, Object>> guns = new ArrayList<>();
+            for (MockGunState gunState : client.getAllGunStates()) {
+                Map<String, Object> gunInfo = new LinkedHashMap<>();
+                gunInfo.put("gunNo", gunState.getGunNo());
+                gunInfo.put("status", gunState.isCharging() ? "charging" : "idle");
+                gunInfo.put("ratedPower", gunState.getRatedPower());
+                gunInfo.put("outputPower", String.format("%.2f", gunState.getOutputPower()));
+                gunInfo.put("outputVoltage", String.format("%.1f", gunState.getOutputVoltage()));
+                gunInfo.put("outputCurrent", String.format("%.1f", gunState.getOutputCurrent()));
+                gunInfo.put("soc", gunState.getSoc());
+                gunInfo.put("chargingEnergy", String.format("%.4f", gunState.getChargingEnergy()));
+                gunInfo.put("chargingTime", gunState.getChargingTime());
+                gunInfo.put("transactionNo", gunState.getTransactionNo());
+                guns.add(gunInfo);
+            }
+            clientInfo.put("guns", guns);
+
+            result.add(clientInfo);
+        }
+
+        return result;
+    }
+
+    /**
+     * 获取指定枪的状态
+     */
+    @GetMapping("/gun/{pileCode}/{gunNo}")
+    public Map<String, Object> getGunStatus(
+            @PathVariable String pileCode,
+            @PathVariable String gunNo) {
+
+        MockGunState gunState = clientManager.getGunState(pileCode, gunNo);
+        if (gunState == null) {
+            Map<String, Object> error = new HashMap<>();
+            error.put("error", "未找到指定的枪");
+            error.put("pileCode", pileCode);
+            error.put("gunNo", gunNo);
+            return error;
+        }
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("pileCode", gunState.getPileCode());
+        result.put("gunNo", gunState.getGunNo());
+        result.put("fullGunNo", gunState.getFullGunNo());
+        result.put("status", gunState.isCharging() ? "charging" : "idle");
+        result.put("gunConnected", gunState.isGunConnected());
+        result.put("ratedPower", gunState.getRatedPower());
+        result.put("maxOutputPowerPercent", gunState.getMaxOutputPowerPercent());
+        result.put("outputPower", String.format("%.2f", gunState.getOutputPower()));
+        result.put("outputVoltage", String.format("%.1f", gunState.getOutputVoltage()));
+        result.put("outputCurrent", String.format("%.1f", gunState.getOutputCurrent()));
+        result.put("gunTemperature", gunState.getGunTemperature());
+        result.put("soc", gunState.getSoc());
+        result.put("batteryMaxTemp", gunState.getBatteryMaxTemp());
+        result.put("chargingEnergy", String.format("%.4f", gunState.getChargingEnergy()));
+        result.put("chargedAmount", String.format("%.4f", gunState.getChargedAmount()));
+        result.put("chargingTime", gunState.getChargingTime());
+        result.put("remainingTime", gunState.getRemainingTime());
+        result.put("transactionNo", gunState.getTransactionNo());
+        result.put("vin", gunState.getVin());
+        result.put("hardwareFault", gunState.getHardwareFault());
+
+        return result;
+    }
+
+    /**
+     * 手动开始充电
+     */
+    @PostMapping("/start/{pileCode}/{gunNo}")
+    public Map<String, Object> startCharging(
+            @PathVariable String pileCode,
+            @PathVariable String gunNo) {
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("pileCode", pileCode);
+        result.put("gunNo", gunNo);
+
+        try {
+            clientManager.startCharging(pileCode, gunNo);
+            result.put("success", true);
+            result.put("message", "充电已启动");
+        } catch (Exception e) {
+            result.put("success", false);
+            result.put("message", e.getMessage());
+        }
+
+        return result;
+    }
+
+    /**
+     * 手动停止充电
+     */
+    @PostMapping("/stop/{pileCode}/{gunNo}")
+    public Map<String, Object> stopCharging(
+            @PathVariable String pileCode,
+            @PathVariable String gunNo) {
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("pileCode", pileCode);
+        result.put("gunNo", gunNo);
+
+        try {
+            clientManager.stopCharging(pileCode, gunNo);
+            result.put("success", true);
+            result.put("message", "充电已停止");
+        } catch (Exception e) {
+            result.put("success", false);
+            result.put("message", e.getMessage());
+        }
+
+        return result;
+    }
+
+    /**
+     * 获取所有枪的状态列表
+     */
+    @GetMapping("/guns")
+    public List<Map<String, Object>> getAllGuns() {
+        List<Map<String, Object>> result = new ArrayList<>();
+
+        for (MockGunState gunState : clientManager.getAllGunStates()) {
+            Map<String, Object> gunInfo = new LinkedHashMap<>();
+            gunInfo.put("fullGunNo", gunState.getFullGunNo());
+            gunInfo.put("pileCode", gunState.getPileCode());
+            gunInfo.put("gunNo", gunState.getGunNo());
+            gunInfo.put("status", gunState.isCharging() ? "充电中" : "空闲");
+            gunInfo.put("ratedPower", gunState.getRatedPower() + "kW");
+            gunInfo.put("outputPower", String.format("%.1fkW", gunState.getOutputPower()));
+            gunInfo.put("soc", gunState.getSoc() + "%");
+            gunInfo.put("energy", String.format("%.2fkWh", gunState.getChargingEnergy()));
+            result.add(gunInfo);
+        }
+
+        return result;
+    }
+}

+ 264 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mock/TestController.java

@@ -0,0 +1,264 @@
+/*
+ * 文 件 名:  GeekOpenCbController
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/2/27
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.mock;
+
+import com.alibaba.fastjson.JSON;
+import com.huashe.common.exception.BusinessException;
+import com.ruoyi.ems.ba.config.BaCtlConfig;
+import com.ruoyi.ems.ba.retrofit.BaCtlEnergyTemplate;
+import com.ruoyi.ems.ba.handle.BaCtlHandler;
+import com.ruoyi.ems.geekopen.handle.GeekOpenCbHandler;
+import com.ruoyi.ems.keka.handle.Keka86BsHandler;
+import com.ruoyi.ems.common.model.CallData;
+import com.ruoyi.ems.model.CallResponse;
+import com.ruoyi.ems.keka.model.ModbusCommand;
+import com.ruoyi.ems.ba.model.CodesValReq;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * Test
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/2/27]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@RestController
+@CrossOrigin(allowedHeaders = "*", allowCredentials = "false")
+@RequestMapping("/test")
+@Api(value = "TestController", description = "测试Api")
+public class TestController {
+    /**
+     * 日志
+     */
+    private static final Logger log = LoggerFactory.getLogger(TestController.class);
+
+    @Qualifier("geekOpenCbHandler")
+    @Resource
+    private GeekOpenCbHandler geekOpenCbHandler;
+
+    @Qualifier("baCtlHandler")
+    @Resource
+    private BaCtlHandler baCtlHandler;
+
+    @Qualifier("keka86BsHandler")
+    @Resource
+    private Keka86BsHandler keka86BsHandler;
+
+    @Resource
+    private BaCtlConfig baCtlConfig;
+
+    /**
+     * msgHandle 消息处理
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/msgHandle", method = RequestMethod.POST)
+    @ApiOperation(value = "/msgHandle", notes = "mqtt消息处理")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public CallResponse<Void> msgHandle(@RequestParam(name = "deviceCode") String deviceCode,
+        @RequestBody String jsonObject) {
+        CallResponse<Void> res = null;
+
+        try {
+            geekOpenCbHandler.msgHandle(deviceCode, jsonObject);
+            res = new CallResponse<>(0, "success");
+        }
+        catch (BusinessException e) {
+            res = new CallResponse<>(500, e.getMessage());
+        }
+        catch (Exception e) {
+            log.error("geekOpenCbAbilityCall fail!", e);
+            res = new CallResponse<>(501, "内部错误:" + e.getMessage());
+        }
+
+        return res;
+    }
+
+    /**
+     * 小时产出
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/collect", method = RequestMethod.GET)
+    @ApiOperation(value = "/collect", notes = "采集触发")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public CallResponse<Void> collect() {
+        CallResponse<Void> res = null;
+
+        try {
+            baCtlHandler.meterCollect();
+            res = new CallResponse<>(0, "success");
+        }
+        catch (BusinessException e) {
+            res = new CallResponse<>(500, e.getMessage());
+        }
+        catch (Exception e) {
+            log.error("collect fail!", e);
+            res = new CallResponse<>(501, "内部错误:" + e.getMessage());
+        }
+
+        return res;
+    }
+
+    /**
+     * 小时产出
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/meterHourProd", method = RequestMethod.GET)
+    @ApiOperation(value = "/meterHourProd", notes = "小时计量触发")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public CallResponse<Void> meterHourProd() {
+        CallResponse<Void> res = null;
+
+        try {
+            baCtlHandler.meterHourProd();
+            res = new CallResponse<>(0, "success");
+        }
+        catch (BusinessException e) {
+            res = new CallResponse<>(500, e.getMessage());
+        }
+        catch (Exception e) {
+            log.error("meterHourProd fail!", e);
+            res = new CallResponse<>(501, "内部错误:" + e.getMessage());
+        }
+
+        return res;
+    }
+
+    /**
+     * 小时产出
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/baGetCodesVal", method = RequestMethod.GET)
+    @ApiOperation(value = "/baGetCodesVal", notes = "BA采集")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public String baGetCodesVal(@RequestParam("pointId") String pointId) {
+        String ret = null;
+
+        try {
+            BaCtlEnergyTemplate template = new BaCtlEnergyTemplate(baCtlConfig.getUrl());
+            CallData<String> callData = template.getCodesVal(new CodesValReq(StringUtils.split(pointId, ",")));
+            ret = callData.getResPayload();
+        }
+        catch (Exception e) {
+            log.error("baGetCodesVal fail!", e);
+            ret = JSON.toJSONString(new CallResponse<>(500, e.getMessage()));
+        }
+
+        return ret;
+    }
+
+    /**
+     * 小时产出
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/baXfCollect", method = RequestMethod.GET)
+    @ApiOperation(value = "/baXfCollect", notes = "BA新风采集")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public String xfCollect() {
+        String ret = null;
+
+        try {
+            baCtlHandler.wtCollect();
+            baCtlHandler.wpCollect();
+            baCtlHandler.lightCollect();
+        }
+        catch (Exception e) {
+            log.error("xfCollect fail!", e);
+            ret = JSON.toJSONString(new CallResponse<>(500, e.getMessage()));
+        }
+
+        return ret;
+    }
+
+    /**
+     * keka86BsSet
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/keka86BsSet", method = RequestMethod.GET)
+    @ApiOperation(value = "/keka86BsSet", notes = "keka86BsSet")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public String keka86BsSet(@RequestParam("gatewayId") String gatewayId, @RequestParam("lightId") Integer lightId,
+        @RequestParam("state") Integer state) {
+        String ret = null;
+
+        try {
+            ModbusCommand payload = keka86BsHandler.buildControlCommand(lightId, state);
+            keka86BsHandler.sendMqttHex("/sc/dtu/ctl/" + gatewayId, payload);
+            ret = JSON.toJSONString(new CallResponse<>(0, "success"));
+        }
+        catch (Exception e) {
+            log.error("keka86BsSet fail!", e);
+            ret = JSON.toJSONString(new CallResponse<>(500, e.getMessage()));
+        }
+
+        return ret;
+    }
+
+    /**
+     * keka86BsSet
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/keka86BsGet", method = RequestMethod.GET)
+    @ApiOperation(value = "/keka86BsGet", notes = "keka86BsGet")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public String keka86BsGet(@RequestParam("gatewayId") String gatewayId, @RequestParam("lightId") Integer lightId) {
+        String ret = null;
+
+        try {
+            ModbusCommand payload = keka86BsHandler.buildReadCommand(lightId);
+            keka86BsHandler.sendMqttHex("/sc/dtu/ctl/" + gatewayId, payload);
+            ret = JSON.toJSONString(new CallResponse<>(0, "success"));
+        }
+        catch (Exception e) {
+            log.error("keka86BsGet fail!", e);
+            ret = JSON.toJSONString(new CallResponse<>(500, e.getMessage()));
+        }
+
+        return ret;
+    }
+}

+ 123 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/model/CallData.java

@@ -0,0 +1,123 @@
+/*
+ * 文 件 名:  CommonCall
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/24
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.model;
+
+import lombok.Getter;
+
+import java.util.List;
+
+/**
+ * CallData
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/24]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+public class CallData<T> {
+    @Getter
+    private final String callPayload;
+
+    @Getter
+    private final String resPayload;
+
+    @Getter
+    private final Integer callStatus;
+
+    @Getter
+    private final T data;
+
+    @Getter
+    private final List<T> list;
+
+    // 私有构造方法,只能通过Builder创建对象
+    private CallData(Builder<T> builder) {
+        this.callPayload = builder.callPayload.toString();
+        this.resPayload = builder.resPayload.toString();
+        this.callStatus = builder.callStatus;
+        this.data = builder.data;
+        this.list = builder.list;
+    }
+
+    // Builder类
+    public static class Builder<T> {
+        private StringBuilder callPayload = new StringBuilder();
+
+        private StringBuilder resPayload = new StringBuilder();
+
+        private Integer callStatus;
+
+        private T data;
+
+        private List<T> list;
+
+        public Builder() {
+        }
+
+        /**
+         * 链式添加callPayload键值对
+         */
+        public Builder<T> addCallParam(String key, Object value) {
+            this.callPayload.append(key).append(": ").append(value).append(System.lineSeparator());
+            return this;
+        }
+
+        /**
+         * 设置resPayload
+         */
+        public Builder<T> setResPayload(String resPayload) {
+            this.resPayload = new StringBuilder(resPayload);
+            return this;
+        }
+
+        public Builder<T> appendResPayload(String resPayload) {
+            this.resPayload.append(resPayload);
+            return this;
+        }
+
+        /**
+         * 设置resPayload
+         */
+        public Builder<T> setCallStatus(Integer callStatus) {
+            this.callStatus = callStatus;
+            return this;
+        }
+
+        /**
+         * 设置data
+         */
+        public Builder<T> setData(T data) {
+            this.data = data;
+            return this;
+        }
+
+        /**
+         * 设置data
+         */
+        public Builder<T> setList(List<T> list) {
+            this.list = list;
+            return this;
+        }
+
+        /**
+         * 构建CallData对象
+         */
+        public CallData<T> build() {
+            return new CallData<>(this);
+        }
+    }
+
+    // 用于获取Builder实例的静态方法
+    public static <T> Builder<T> builder() {
+        return new Builder<>();
+    }
+}

+ 28 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mqtt/MqttMessageHandler.java

@@ -0,0 +1,28 @@
+/*
+ * 文 件 名:  MqttHandler
+ * 版    权:  浩鲸云计算科技股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2024/4/30
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.mqtt;
+
+/**
+ * Mqtt消息处理
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2024/4/30]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+public interface MqttMessageHandler {
+    /**
+     * 消息出来
+     * @param payload 消息报文
+     */
+    void handle(String topic, byte[] payload);
+}

+ 145 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/mqtt/MqttTemplate.java

@@ -0,0 +1,145 @@
+/*
+ * 文 件 名:  MqttTemplate
+ * 版    权:  浩鲸云计算科技股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2024/4/30
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.mqtt;
+
+import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
+import org.eclipse.paho.client.mqttv3.MqttCallback;
+import org.eclipse.paho.client.mqttv3.MqttClient;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Mqtt操作模板
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2024/4/30]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+public class MqttTemplate {
+    private static final Logger log = LoggerFactory.getLogger(MqttTemplate.class);
+
+    private MqttClient mqttClient;
+
+    /**
+     * 构造器
+     *
+     * @param broker   服务地址
+     * @param clientId 客户端编码
+     */
+    public MqttTemplate(String broker, String clientId) throws MqttException {
+        MemoryPersistence persistence = new MemoryPersistence();
+
+        mqttClient = new MqttClient(broker, clientId, persistence);
+        // MQTT 连接选项
+        MqttConnectOptions connOpts = new MqttConnectOptions();
+        // 保留会话
+        connOpts.setCleanSession(false);
+        connOpts.setAutomaticReconnect(true);
+        // 建立连接
+        mqttClient.connect(connOpts);
+    }
+
+    /**
+     * 订阅
+     *
+     * @param topic 主题
+     * @param qos   0-最多1次,1-至少1次,2-仅1次
+     */
+    public void subscribe(String topic, Integer qos, MqttMessageHandler handler) {
+        try {
+            mqttClient.subscribe(topic, qos);
+            mqttClient.setCallback(new MqttCallback() {
+                // 链接丢失处理
+                @Override
+                public void connectionLost(Throwable throwable) {
+                    // 连接丢失后,一般在这里面进行重连
+                    log.info("mqtt服务连接断开,进行重连");
+
+                    try {
+                        // 尝试重新连接
+                        if (!mqttClient.isConnected()) {
+                            mqttClient.reconnect();
+                        }
+                    }
+                    catch (MqttException e) {
+                        log.error("重连失败:[{}]", e.getMessage());
+                    }
+                }
+
+                // 消息接收处理
+                @Override
+                public void messageArrived(String topic, MqttMessage message) throws Exception {
+                    byte[] payload = message.getPayload();
+                    handler.handle(topic, payload);
+                }
+
+                // 消息发送处理
+                @Override
+                public void deliveryComplete(IMqttDeliveryToken token) {
+                    log.info("消息发布结果[{}]", token.isComplete());
+                }
+            });
+        }
+        catch (Exception e) {
+            log.error("mqtt订阅异常", e);
+        }
+    }
+
+    /**
+     * 消息发送
+     *
+     * @param topic    主题
+     * @param payload  报文
+     * @param qos      消息级别
+     * @param retained 回复
+     */
+    public void sendString(String topic, String payload, int qos, boolean retained) {
+        try {
+            MqttMessage message = new MqttMessage();
+            message.setPayload(payload.getBytes(StandardCharsets.UTF_8));
+            message.setQos(qos);
+            message.setRetained(retained);
+            mqttClient.publish(topic, message);
+        }
+        catch (MqttException e) {
+            log.error("[Send]fail!", e);
+        }
+    }
+
+    /**
+     * 发送十六进制数据 - 优化版本,直接接收字节数组
+     *
+     * @param topic       主题
+     * @param payloadBytes 十六进制字节数组
+     * @param qos         消息级别
+     * @param retained    回复
+     */
+    public void sendHex(String topic, byte[] payloadBytes, int qos, boolean retained) {
+        try {
+            MqttMessage message = new MqttMessage();
+            message.setPayload(payloadBytes);
+            message.setQos(qos);
+            message.setRetained(retained);
+            mqttClient.publish(topic, message);
+        }
+        catch (MqttException e) {
+            log.error("[SendHex]fail!", e);
+        }
+    }
+}

+ 53 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/retrofit/BaseApiTemplate.java

@@ -0,0 +1,53 @@
+/*
+ * 文 件 名:  BaseApiTemplate
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/7/19
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.common.retrofit;
+
+import okhttp3.OkHttpClient;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Api 操作模版基类
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/7/19]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+public class BaseApiTemplate {
+    /**
+     * 连接超时(毫秒)
+     */
+    protected int connectTimeout = 60000;
+
+    /**
+     * 读取超时(毫秒)
+     */
+    protected int readTimeout = 60000;
+
+    /**
+     * 写超时(毫秒)
+     */
+    protected int writeTimeout = 60000;
+
+    /**
+     * 创建连接
+     *
+     * @return 连接
+     */
+    protected OkHttpClient getClient() {
+        OkHttpClient.Builder builder = new OkHttpClient().newBuilder()
+            .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS).readTimeout(readTimeout, TimeUnit.MILLISECONDS)
+            .writeTimeout(writeTimeout, TimeUnit.MILLISECONDS);
+        return builder.build();
+    }
+}

+ 87 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/retrofit/EmsApi.java

@@ -0,0 +1,87 @@
+/*
+ * 文 件 名:  EmsApi
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  EMS Server API 接口定义
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/9
+ */
+package com.ruoyi.ems.common.retrofit;
+
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.GET;
+import retrofit2.http.Headers;
+import retrofit2.http.POST;
+import retrofit2.http.Query;
+
+import java.util.Map;
+
+/**
+ * EMS Server API 接口
+ * 用于 ems-dev-adapter 调用 ems-server 的策略相关接口
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/9]
+ */
+public interface EmsApi {
+
+    /**
+     * 属性值变化通知(单个)
+     *
+     * @param objCode  设备代码
+     * @param attrKey  属性键
+     * @param newValue 新值
+     * @param oldValue 旧值(可选)
+     * @return 响应
+     */
+    @Headers({"Content-Type: application/json"})
+    @POST("/ems/energyStrategy/onAttrValueChanged")
+    Call<String> onAttrValueChanged(
+        @Query("objCode") String objCode,
+        @Query("attrKey") String attrKey,
+        @Query("newValue") String newValue,
+        @Query("oldValue") String oldValue
+    );
+
+    /**
+     * 属性值变化通知(批量)
+     *
+     * @param request 请求体 { "objCode": "xxx", "attributes": { "key1": "val1", ... } }
+     * @return 响应
+     */
+    @Headers({"Content-Type: application/json"})
+    @POST("/ems/energyStrategy/onAttrValueChangedBatch")
+    Call<String> onAttrValueChangedBatch(@Body Map<String, Object> request);
+
+    /**
+     * 设备事件通知
+     *
+     * @param objCode   设备代码
+     * @param eventKey  事件键
+     * @param eventData 事件数据
+     * @return 响应
+     */
+    @Headers({"Content-Type: application/json"})
+    @POST("/ems/energyStrategy/onDeviceEvent")
+    Call<String> onDeviceEvent(
+        @Query("objCode") String objCode,
+        @Query("eventKey") String eventKey,
+        @Body Map<String, Object> eventData
+    );
+
+    /**
+     * 获取调度器状态
+     *
+     * @return 响应
+     */
+    @GET("/ems/energyStrategy/scheduler/status")
+    Call<String> getSchedulerStatus();
+
+    /**
+     * 重新加载调度器
+     *
+     * @return 响应
+     */
+    @POST("/ems/energyStrategy/scheduler/reload")
+    Call<String> reloadScheduler();
+}

+ 348 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/common/retrofit/EmsApiTemplate.java

@@ -0,0 +1,348 @@
+/*
+ * 文 件 名:  EmsApiTemplate
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  EMS Server API 调用模板
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/9
+ */
+package com.ruoyi.ems.common.retrofit;
+
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.support.retrofit.Retrofit2ConverterFactory;
+import com.ruoyi.ems.common.config.EmsConfig;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.OkHttpClient;
+import retrofit2.Call;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * EMS Server API 调用模板
+ * 用于 ems-dev-adapter 调用 ems-server 的策略相关接口
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/9]
+ */
+@Slf4j
+public class EmsApiTemplate extends BaseApiTemplate {
+    /**
+     * EMS Server 地址
+     */
+    protected String url;
+
+    private boolean notifyEnabled;
+
+    /**
+     * API 代理
+     */
+    protected final EmsApi api;
+
+    /**
+     * 异步执行线程池
+     */
+    private final ExecutorService asyncExecutor = Executors.newFixedThreadPool(3);
+
+    /**
+     * 构造调用模板(默认超时配置)
+     *
+     * @param emsConfig EMS Server 地址 (http://ip:port)
+     */
+    public EmsApiTemplate(EmsConfig emsConfig) {
+        super.connectTimeout = emsConfig.getConnectTimeout();
+        super.readTimeout = emsConfig.getReadTimeout();
+        super.writeTimeout = emsConfig.getWriteTimeout();
+        this.url = emsConfig.getUrl();
+        this.notifyEnabled = emsConfig.isNotifyEnabled();
+
+        OkHttpClient httpClient = getClient();
+        Retrofit retrofit = new Retrofit.Builder().baseUrl(this.url)
+            .addConverterFactory(Retrofit2ConverterFactory.create()).client(httpClient).build();
+        this.api = retrofit.create(EmsApi.class);
+    }
+
+    // ==================== 同步调用方法 ====================
+
+    /**
+     * 通知属性值变化(同步)
+     *
+     * @param objCode  设备代码
+     * @param attrKey  属性键
+     * @param oldValue 旧值(可为null)
+     * @param newValue 新值
+     * @return 触发的策略数量,失败返回 -1
+     */
+    public int notifyAttrValueChanged(String objCode, String attrKey, Object oldValue, Object newValue) {
+        if (!notifyEnabled) {
+            log.debug("策略通知已禁用,跳过: obj={}, attr={}", objCode, attrKey);
+            return 0;
+        }
+
+        try {
+            String oldValueStr = oldValue != null ? String.valueOf(oldValue) : null;
+            String newValueStr = newValue != null ? String.valueOf(newValue) : "";
+
+            Call<String> call = api.onAttrValueChanged(objCode, attrKey, newValueStr, oldValueStr);
+            Response<String> response = call.execute();
+
+            log.debug("notifyAttrValueChanged response: {}", response.body());
+
+            if (response.isSuccessful() && response.body() != null) {
+                JSONObject resJson = JSONObject.parseObject(response.body());
+                Integer code = resJson.getInteger("code");
+
+                if (code != null && code == 200) {
+                    JSONObject data = resJson.getJSONObject("data");
+                    int triggeredCount = data != null ? data.getIntValue("triggeredStrategies") : 0;
+                    log.debug("属性变化通知成功: obj={}, attr={}, {} -> {}, triggered={}", objCode, attrKey, oldValue,
+                        newValue, triggeredCount);
+                    return triggeredCount;
+                }
+                else {
+                    log.warn("属性变化通知失败: code={}, msg={}", code, resJson.getString("msg"));
+                }
+            }
+            else {
+                log.warn("属性变化通知HTTP失败: status={}, body={}", response.code(), response.body());
+            }
+        }
+        catch (Exception e) {
+            log.error("属性变化通知异常: obj={}, attr={}, error={}", objCode, attrKey, e.getMessage());
+        }
+
+        return -1;
+    }
+
+    /**
+     * 批量通知属性值变化(同步)
+     *
+     * @param objCode    设备代码
+     * @param attributes 属性键值对
+     * @return 触发的策略数量,失败返回 -1
+     */
+    public int notifyAttrValueChangedBatch(String objCode, Map<String, Object> attributes) {
+        if (!notifyEnabled) {
+            log.debug("策略通知已禁用,跳过批量通知: obj={}", objCode);
+            return 0;
+        }
+
+        if (attributes == null || attributes.isEmpty()) {
+            log.debug("属性为空,跳过批量通知: obj={}", objCode);
+            return 0;
+        }
+
+        try {
+            Map<String, Object> request = new HashMap<>();
+            request.put("objCode", objCode);
+            request.put("attributes", attributes);
+
+            Call<String> call = api.onAttrValueChangedBatch(request);
+            Response<String> response = call.execute();
+
+            log.debug("notifyAttrValueChangedBatch response: {}", response.body());
+
+            if (response.isSuccessful() && response.body() != null) {
+                JSONObject resJson = JSONObject.parseObject(response.body());
+                Integer code = resJson.getInteger("code");
+
+                if (code != null && code == 200) {
+                    JSONObject data = resJson.getJSONObject("data");
+                    int triggeredCount = data != null ? data.getIntValue("triggeredStrategies") : 0;
+                    log.debug("批量属性变化通知成功: obj={}, attrCount={}, triggered={}", objCode, attributes.size(),
+                        triggeredCount);
+                    return triggeredCount;
+                }
+                else {
+                    log.warn("批量属性变化通知失败: code={}, msg={}", code, resJson.getString("msg"));
+                }
+            }
+            else {
+                log.warn("批量属性变化通知HTTP失败: status={}", response.code());
+            }
+        }
+        catch (Exception e) {
+            log.error("批量属性变化通知异常: obj={}, error={}", objCode, e.getMessage());
+        }
+
+        return -1;
+    }
+
+    /**
+     * 通知设备事件(同步)
+     *
+     * @param objCode   设备代码
+     * @param eventKey  事件键
+     * @param eventData 事件数据(可为null)
+     * @return 触发的策略数量,失败返回 -1
+     */
+    public int notifyDeviceEvent(String objCode, String eventKey, Map<String, Object> eventData) {
+        if (!notifyEnabled) {
+            log.debug("策略通知已禁用,跳过事件通知: obj={}, event={}", objCode, eventKey);
+            return 0;
+        }
+
+        try {
+            Map<String, Object> data = eventData != null ? eventData : new HashMap<>();
+
+            Call<String> call = api.onDeviceEvent(objCode, eventKey, data);
+            Response<String> response = call.execute();
+
+            log.debug("notifyDeviceEvent response: {}", response.body());
+
+            if (response.isSuccessful() && response.body() != null) {
+                JSONObject resJson = JSONObject.parseObject(response.body());
+                Integer code = resJson.getInteger("code");
+
+                if (code != null && code == 200) {
+                    JSONObject respData = resJson.getJSONObject("data");
+                    int triggeredCount = respData != null ? respData.getIntValue("triggeredStrategies") : 0;
+                    log.debug("设备事件通知成功: obj={}, event={}, triggered={}", objCode, eventKey, triggeredCount);
+                    return triggeredCount;
+                }
+                else {
+                    log.warn("设备事件通知失败: code={}, msg={}", code, resJson.getString("msg"));
+                }
+            }
+            else {
+                log.warn("设备事件通知HTTP失败: status={}", response.code());
+            }
+        }
+        catch (Exception e) {
+            log.error("设备事件通知异常: obj={}, event={}, error={}", objCode, eventKey, e.getMessage());
+        }
+
+        return -1;
+    }
+
+    /**
+     * 获取调度器状态(同步)
+     *
+     * @return 调度器状态信息,失败返回 null
+     */
+    public JSONObject getSchedulerStatus() {
+        try {
+            Call<String> call = api.getSchedulerStatus();
+            Response<String> response = call.execute();
+
+            log.debug("getSchedulerStatus response: {}", response.body());
+
+            if (response.isSuccessful() && response.body() != null) {
+                JSONObject resJson = JSONObject.parseObject(response.body());
+                Integer code = resJson.getInteger("code");
+
+                if (code != null && code == 200) {
+                    return resJson.getJSONObject("data");
+                }
+                else {
+                    log.warn("获取调度器状态失败: code={}, msg={}", code, resJson.getString("msg"));
+                }
+            }
+            else {
+                log.warn("获取调度器状态HTTP失败: status={}", response.code());
+            }
+        }
+        catch (Exception e) {
+            log.error("获取调度器状态异常: error={}", e.getMessage());
+        }
+
+        return null;
+    }
+
+    /**
+     * 重新加载调度器(同步)
+     *
+     * @return 是否成功
+     */
+    public boolean reloadScheduler() {
+        try {
+            Call<String> call = api.reloadScheduler();
+            Response<String> response = call.execute();
+
+            log.debug("reloadScheduler response: {}", response.body());
+
+            if (response.isSuccessful() && response.body() != null) {
+                JSONObject resJson = JSONObject.parseObject(response.body());
+                Integer code = resJson.getInteger("code");
+                return code != null && code == 200;
+            }
+        }
+        catch (Exception e) {
+            log.error("重新加载调度器异常: error={}", e.getMessage());
+        }
+
+        return false;
+    }
+
+    // ==================== 异步调用方法 ====================
+
+    /**
+     * 通知属性值变化(异步)
+     * 不阻塞主流程,适合在数据采集时使用
+     *
+     * @param objCode  设备代码
+     * @param attrKey  属性键
+     * @param oldValue 旧值
+     * @param newValue 新值
+     */
+    public void notifyAttrValueChangedAsync(String objCode, String attrKey, Object oldValue, Object newValue) {
+        if (!notifyEnabled) {
+            return;
+        }
+
+        asyncExecutor.submit(() -> {
+            try {
+                notifyAttrValueChanged(objCode, attrKey, oldValue, newValue);
+            }
+            catch (Exception e) {
+                log.error("异步属性变化通知异常: obj={}, attr={}", objCode, attrKey, e);
+            }
+        });
+    }
+
+    /**
+     * 批量通知属性值变化(异步)
+     *
+     * @param objCode    设备代码
+     * @param attributes 属性键值对
+     */
+    public void notifyAttrValueChangedBatchAsync(String objCode, Map<String, Object> attributes) {
+        if (!notifyEnabled || attributes == null || attributes.isEmpty()) {
+            return;
+        }
+
+        asyncExecutor.submit(() -> {
+            try {
+                notifyAttrValueChangedBatch(objCode, attributes);
+            }
+            catch (Exception e) {
+                log.error("异步批量属性变化通知异常: obj={}", objCode, e);
+            }
+        });
+    }
+
+    /**
+     * 通知设备事件(异步)
+     *
+     * @param objCode   设备代码
+     * @param eventKey  事件键
+     * @param eventData 事件数据
+     */
+    public void notifyDeviceEventAsync(String objCode, String eventKey, Map<String, Object> eventData) {
+        if (!notifyEnabled) {
+            return;
+        }
+
+        asyncExecutor.submit(() -> {
+            try {
+                notifyDeviceEvent(objCode, eventKey, eventData);
+            }
+            catch (Exception e) {
+                log.error("异步设备事件通知异常: obj={}, event={}", objCode, eventKey, e);
+            }
+        });
+    }
+}

+ 86 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/geekopen/MqttCacheMsg.java

@@ -0,0 +1,86 @@
+/*
+ * 文 件 名:  BaseMsg
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/2/27
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.geekopen;
+
+import java.util.Date;
+
+/**
+ * Mqtt交互消息
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/2/27]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+public class MqttCacheMsg {
+    /**
+     * 消息ID
+     */
+    private String messageId;
+
+    /**
+     * 设备代码
+     */
+    private String deviceCode;
+
+    /**
+     * 接收内容
+     */
+    private String payload;
+
+    /**
+     * 接收时间
+     */
+    private Date receiveTime;
+
+    public MqttCacheMsg() {
+    }
+
+    public MqttCacheMsg(String messageId, String deviceCode, Date date, String receivePayload) {
+        this.messageId = messageId;
+        this.deviceCode = deviceCode;
+        this.receiveTime = date;
+        this.payload = receivePayload;
+    }
+
+    public String getMessageId() {
+        return messageId;
+    }
+
+    public void setMessageId(String messageId) {
+        this.messageId = messageId;
+    }
+
+    public String getDeviceCode() {
+        return deviceCode;
+    }
+
+    public void setDeviceCode(String deviceCode) {
+        this.deviceCode = deviceCode;
+    }
+
+    public String getPayload() {
+        return payload;
+    }
+
+    public void setPayload(String payload) {
+        this.payload = payload;
+    }
+
+    public Date getReceiveTime() {
+        return receiveTime;
+    }
+
+    public void setReceiveTime(Date receiveTime) {
+        this.receiveTime = receiveTime;
+    }
+}

+ 65 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/geekopen/ObjectCache.java

@@ -0,0 +1,65 @@
+/*
+ * 文 件 名:  MessageCache
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/2/27
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.geekopen;
+
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 消息缓存
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/2/27]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Service
+public class ObjectCache {
+    /**
+     * 消息缓存队列
+     */
+    private final static Map<String, MqttCacheMsg> devResCache = new ConcurrentHashMap<>();
+
+    /**
+     * 清理过期的设备响应
+     */
+    public int cleanDevResCache() {
+        int cnt = 0;
+        long currentTime = new Date().getTime();
+        long threshold = 5 * 60 * 1000; // 分钟
+
+        Iterator<Map.Entry<String, MqttCacheMsg>> iterator = devResCache.entrySet().iterator();
+        while (iterator.hasNext()) {
+            Map.Entry<String, MqttCacheMsg> entry = iterator.next();
+            MqttCacheMsg msg = entry.getValue();
+            if ((currentTime - msg.getReceiveTime().getTime()) > threshold) {
+                // 删除缓存中的消息
+                iterator.remove();
+                cnt++;
+            }
+        }
+
+        return cnt;
+    }
+
+    public void setDevResCache(String messageId, MqttCacheMsg mqttMessage) {
+        devResCache.put(messageId, mqttMessage);
+    }
+
+    public MqttCacheMsg readDevResponse(String messageId) {
+        return devResCache.remove(messageId);
+    }
+}

+ 77 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/geekopen/controller/CircuitBreakerController.java

@@ -0,0 +1,77 @@
+/*
+ * 文 件 名:  GeekOpenCbController
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/2/27
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.geekopen.controller;
+
+import com.ruoyi.ems.common.handle.BaseMeterDevHandler;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * 断路器接口
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/2/27]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@RestController
+@CrossOrigin(allowedHeaders = "*", allowCredentials = "false")
+@RequestMapping("/circuit-breaker")
+@Api(value = "CircuitBreakerController", description = "断路器控制Api")
+public class CircuitBreakerController {
+    /**
+     * 日志
+     */
+    private static final Logger log = LoggerFactory.getLogger(CircuitBreakerController.class);
+
+    @Qualifier("geekOpenCbHandler")
+    @Resource
+    private BaseMeterDevHandler geekOpenCbHandler;
+
+    /**
+     * GeekOpen 断路器能力调用
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/GeekOpen/abilityCall", method = RequestMethod.POST)
+    @ApiOperation(value = "/GeekOpen/call", notes = "GeekOpen 断路器能力调用")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public CallResponse<Void> geekOpenCbAbilityCall(@RequestBody AbilityPayload abilityPayload) {
+        CallResponse<Void> res = null;
+
+        try {
+            res = geekOpenCbHandler.call(abilityPayload);
+        }
+        catch (Exception e) {
+            log.error("geekOpenCbAbilityCall fail!", e);
+            res = new CallResponse<>(501, "内部错误:" + e.getMessage());
+        }
+
+        return res;
+    }
+}

+ 433 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/geekopen/handle/GeekOpenCbHandler.java

@@ -0,0 +1,433 @@
+/*
+ * 文 件 名:  GeekOpenService
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/2/27
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.geekopen.handle;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.huashe.common.domain.JsonEntity;
+import com.huashe.common.exception.Assert;
+import com.huashe.common.exception.BusinessException;
+import com.huashe.common.utils.DateUtils;
+import com.huashe.common.utils.ThreadUtils;
+import com.ruoyi.ems.common.handle.BaseMeterDevHandler;
+import com.ruoyi.ems.common.config.EmsConfig;
+import com.ruoyi.ems.common.retrofit.EmsApiTemplate;
+import com.ruoyi.ems.common.mqtt.MqttTemplate;
+import com.ruoyi.ems.geekopen.ObjectCache;
+import com.ruoyi.ems.domain.EmsDevice;
+import com.ruoyi.ems.domain.EmsObjAbilityCallLog;
+import com.ruoyi.ems.domain.EmsObjAttrValue;
+import com.ruoyi.ems.domain.MeterDevice;
+import com.ruoyi.ems.enums.DevOnlineStatus;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import com.ruoyi.ems.geekopen.MqttCacheMsg;
+import com.ruoyi.ems.model.QueryDevice;
+import com.ruoyi.ems.util.IdUtils;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * GeekOpen 断路器服务层
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/2/27]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Service
+public class GeekOpenCbHandler extends BaseMeterDevHandler {
+    private static final Logger log = LoggerFactory.getLogger(GeekOpenCbHandler.class);
+
+    @Autowired
+    private ObjectCache messageCache;
+
+    @Resource
+    @Qualifier("mqttTemplate")
+    protected MqttTemplate mqttTemplate;
+
+    // 主题前缀
+    private static final String TOPIC_PREFIX = "/device/dlq/";
+
+    // 设备模型代码
+    private static final String MODE_CODE = "M_W2_QF_GEEKOPEN";
+
+    @Autowired
+    private EmsConfig emsConfig;
+
+    /**
+     * 能力执行
+     *
+     * @param abilityParam 执行参数
+     * @return 响应
+     */
+    @Override
+    public CallResponse<Void> call(AbilityPayload abilityParam) {
+        CallResponse<Void> callResponse = null;
+
+        JSONObject msgBody = buildSendMsg(abilityParam);
+        String messageId = msgBody.getString("messageId");
+        String deviceCode = abilityParam.getObjCode();
+
+        // 发送消息到MQTT服务器
+        long sendTime = System.currentTimeMillis();
+        String topic = TOPIC_PREFIX + deviceCode;
+        sendMqttMsg(topic, msgBody.toString(), 2, false);
+
+        // 需要监测回包的流程
+        if (StringUtils.startsWith(messageId, "CALL-")) {
+            // 写入日志
+            EmsObjAbilityCallLog logItem = saveCallLog(abilityParam, msgBody.toString(), sendTime, 1);
+
+            while (true) {
+                MqttCacheMsg cacheMsg = messageCache.readDevResponse(messageId);
+
+                if (null != cacheMsg) {
+                    String receiveParam = cacheMsg.getPayload();
+                    JSONObject receiveObject = JSONObject.parseObject(receiveParam);
+                    callResponse = checkResult(msgBody, receiveObject);
+                    updateCallLog(logItem, cacheMsg, callResponse.getCode() == 0 ? 0 : 2);
+
+                    break;
+                }
+
+                ThreadUtils.sleep(100);
+
+                if (System.currentTimeMillis() - sendTime > 30000) {
+                    logItem.setResPayload("响应超时");
+                    logItem.setCallStatus(2);
+                    objAbilityCallLogService.updateLog(logItem);
+                    callResponse = new CallResponse<>(-1, "响应超时!");
+                    break;
+                }
+            }
+        }
+        else {
+            saveCallLog(abilityParam, msgBody.toString(), sendTime, 0);
+            callResponse = new CallResponse<>(0, "执行成功!");
+        }
+
+        return callResponse;
+    }
+
+    @Async("msgHandleExecutor")
+    public void msgHandle(String deviceCode, String payload) {
+        try {
+            EmsDevice device = deviceService.selectByCode(deviceCode);
+
+            if (null != device) {
+                JSONObject msgBody = JSONObject.parseObject(payload);
+                String messageId = msgBody.getString("messageId");
+
+                // 自动上报数据: 1.更新属性值,2.更新电量计量数据
+                if (StringUtils.equals("auto", messageId)) {
+                    updateAutoAttr(device, msgBody);
+                    saveCallLog(deviceCode, device.getDeviceModel(), "triggerSync", 0, null, payload);
+                }
+                // 前序调用的响应消息:1.写入消息队列,2.更新属性值
+                else if (StringUtils.isNotEmpty(messageId) && StringUtils.startsWith(messageId, "CALL-")) {
+                    MqttCacheMsg mqttCacheMsg = new MqttCacheMsg(messageId, deviceCode, new Date(), payload);
+                    messageCache.setDevResCache(messageId, mqttCacheMsg);
+                    updateBaseAttr(device, msgBody);
+                }
+                // 设备同步数据(INFO类,协议类):更新基础属性值
+                else {
+                    updateBaseAttr(device, msgBody);
+                    saveCallLog(deviceCode, device.getDeviceModel(), "triggerSync", 0, null, payload);
+                }
+
+                // 设备消息抵达,将数据库离线状态改为在线
+                refreshStatus(device, DevOnlineStatus.ONLINE);
+                // 设备消息抵达,更新最后消息时间
+                updateMsgTime(device, new Date());
+            }
+            else {
+                log.warn("接收消息,设备未注册, deviceCode:{}\nmessageBody:{}", deviceCode, payload);
+            }
+        }
+        catch (Exception e) {
+            log.error(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 获取设备列表
+     *
+     * @return 设备列表
+     */
+    @Override
+    public List<EmsDevice> getDeviceList() {
+        QueryDevice queryDevice = new QueryDevice();
+        queryDevice.setDeviceModel(MODE_CODE);
+        return deviceService.selectList(queryDevice);
+    }
+
+    @Override
+    public List<MeterDevice> getMeterDeviceList() {
+        return meterDeviceService.selectByModel("M_W2_QF_GEEKOPEN");
+    }
+
+    /**
+     * 更新基础属性值
+     *
+     * @param device   设备信息
+     * @param jsonBody jsonBody 消息体
+     */
+    private void updateBaseAttr(EmsDevice device, JSONObject jsonBody) {
+        updateAttr(device, jsonBody, "key");
+        updateAttr(device, jsonBody, "iccid");
+        updateAttr(device, jsonBody, "imei");
+        updateAttr(device, jsonBody, "signal");
+        updateAttr(device, jsonBody, "version");
+        updateAttr(device, jsonBody, "timerEnable");
+        updateAttr(device, jsonBody, "timerInterval");
+        updateAttr(device, jsonBody, "keyLock");
+        updateAttr(device, jsonBody, "onState");
+        updateAttr(device, jsonBody, "resetLock");
+
+        // 协议信息
+        updateAttr(device, jsonBody, "protocol");
+        updateAttr(device, jsonBody, "clientId");
+        updateAttr(device, jsonBody, "server");
+        updateAttr(device, jsonBody, "port");
+        updateAttr(device, jsonBody, "username");
+        updateAttr(device, jsonBody, "publish");
+        updateAttr(device, jsonBody, "subcribe");
+    }
+
+    /**
+     * 更新自动上报属性值
+     *
+     * @param device   设备信息
+     * @param jsonBody jsonBody 消息体
+     */
+    private void updateAutoAttr(EmsDevice device, JSONObject jsonBody) {
+        updateAttr(device, jsonBody, "voltage");
+        updateAttr(device, jsonBody, "current");
+        updateAttr(device, jsonBody, "power");
+        updateAttr(device, jsonBody, "energy", NEW_HOUR_READING);
+    }
+
+    public boolean updateAttr(EmsDevice device, JSONObject jsonBody, String sourceKey) {
+        return updateAttr(device, jsonBody, sourceKey, sourceKey);
+    }
+
+    /**
+     * 校验是否需要更新属性值
+     *
+     * @param jsonBody  jsonBody 消息体
+     * @param sourceKey 源属性key
+     * @param targetKey 目标缓存属性key
+     * @return true 更新,false 不更新
+     */
+    public boolean updateAttr(EmsDevice device, JSONObject jsonBody, String sourceKey, String targetKey) {
+        boolean flag = false;
+
+        try {
+            // 校验消息体是否包含属性key
+            if (!jsonBody.containsKey(sourceKey)) {
+                return flag;
+            }
+
+            // 获取属性值
+            String currentValue = jsonBody.getString(sourceKey);
+
+            // 获取缓存
+            String cacheValue = redisService.getCacheMapValue(device.getDeviceCode(), targetKey);
+
+            if (null == cacheValue) {
+                EmsObjAttrValue param = new EmsObjAttrValue();
+                param.setObjCode(device.getDeviceCode());
+                param.setModelCode(device.getDeviceModel());
+                param.setAttrKey(targetKey);
+
+                List<EmsObjAttrValue> attrList = objAttrValueService.selectObjAttrValueList(param);
+                EmsObjAttrValue attrValue = CollectionUtils.isNotEmpty(attrList) ? attrList.get(0) : null;
+                cacheValue = null != attrValue ? attrValue.getAttrValue() : null;
+            }
+
+            // 比较属性值是否发生变化
+            if (!StringUtils.equals(currentValue, cacheValue)) {
+                EmsObjAttrValue attrValue = new EmsObjAttrValue(device.getDeviceCode(), device.getDeviceModel(),
+                    targetKey, currentValue);
+                // 更新数据库
+                objAttrValueService.mergeObjAttrValue(attrValue);
+                // 更新缓存
+                updateCacheAfterSuccess(device.getDeviceCode(), targetKey, currentValue);
+                new EmsApiTemplate(emsConfig).notifyAttrValueChangedAsync(device.getDeviceCode(), targetKey, cacheValue,
+                    currentValue);
+                flag = true;
+            }
+        }
+        catch (Exception e) {
+            log.error("检查属性并更新失败!", e);
+        }
+
+        return flag;
+    }
+
+    private CallResponse<Void> checkResult(JSONObject sendObject, JSONObject receiveObject) {
+        CallResponse<Void> response = new CallResponse<>();
+
+        try {
+            String type = sendObject.getString("type");
+
+            if (StringUtils.equals(type, "event")) {
+                String sendKeyValue = sendObject.getString("key");
+                String receiveKeyValue = receiveObject.getString("key");
+                Assert.isTrue(StringUtils.equals(sendKeyValue, receiveKeyValue), -1, "响应参数key校验失败!");
+            }
+            else if (StringUtils.equals(type, "setting")) {
+                if (sendObject.containsKey("keyLock")) {
+                    String sendKeyLockValue = sendObject.getString("keyLock");
+                    String receiveKeyLockValue = receiveObject.getString("keyLock");
+                    Assert.isTrue(StringUtils.equals(sendKeyLockValue, receiveKeyLockValue), -1,
+                        "响应参数keyLock校验失败!");
+                }
+                else if (sendObject.containsKey("timerEnable") && sendObject.containsKey("timerInterval")) {
+                    String sendTimerEnableValue = sendObject.getString("timerEnable");
+                    String receiveTimerEnableValue = receiveObject.getString("timerEnable");
+                    Assert.isTrue(StringUtils.equals(sendTimerEnableValue, receiveTimerEnableValue), -1,
+                        "响应参数timerEnable校验失败!");
+                    String sendTimerIntervalValue = sendObject.getString("timerInterval");
+                    String receiveTimerIntervalValue = receiveObject.getString("timerInterval");
+                    Assert.isTrue(StringUtils.equals(sendTimerIntervalValue, receiveTimerIntervalValue), -1,
+                        "响应参数timerInterval校验失败!");
+                }
+                else if (sendObject.containsKey("onState")) {
+                    String sendOnStateValue = sendObject.getString("onState");
+                    String receiveOnStateValue = receiveObject.getString("onState");
+                    Assert.isTrue(StringUtils.equals(sendOnStateValue, receiveOnStateValue), -1,
+                        "响应参数onState校验失败!");
+                }
+            }
+
+            response.setCode(0);
+            response.setMessage("执行成功!");
+        }
+        catch (BusinessException e) {
+            log.warn(e.getMessage());
+            response.setCode(e.getCode());
+            response.setMessage(e.getMessage());
+        }
+        catch (Exception e) {
+            log.error(e.getMessage(), e);
+            response.setCode(-1);
+            response.setMessage("内部错误!");
+        }
+
+        return response;
+    }
+
+    /**
+     * 定时刷新在线状态
+     * <br/>每小时执行一次,扫描2个小时无消息的设备,标记为离线状态
+     */
+    @Override
+    public void refreshOnline() {
+        long threshold = 2 * 60 * 60 * 1000;
+        long currentTime = new Date().getTime();
+
+        List<EmsDevice> deviceList = getDeviceList();
+
+        if (CollectionUtils.isNotEmpty(deviceList)) {
+            for (EmsDevice device : deviceList) {
+                String lastMsgTime = redisService.getCacheMapValue(device.getDeviceCode(), MQTT_LAST_TIME);
+
+                if (null != lastMsgTime) {
+                    // 计算最后一次消息至今的时间差
+                    long time =
+                        currentTime - DateUtils.stringToDate(lastMsgTime, DateUtils.YYYY_MM_DD_HH_MM_SS).getTime();
+
+                    if (time > threshold) {
+                        refreshStatus(device, DevOnlineStatus.OFFLINE);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 构造下发指令
+     *
+     * @param abilityParam 消息
+     * @return msgBody
+     */
+    public JSONObject buildSendMsg(AbilityPayload abilityParam) {
+        String abilityKey = abilityParam.getAbilityKey();
+        JSONObject jsonBody = null;
+
+        if (StringUtils.equals("OnOffCtl", abilityKey)) {
+            jsonBody = JsonEntity.objBuilder().putKv("type", "event")
+                .putKv("key", Integer.parseInt(abilityParam.getAbilityParam()))
+                .putKv("messageId", "CALL-" + IdUtils.generateMessageId()).build().getJsonObj();
+        }
+        else if (StringUtils.equals("KeyLockCtl", abilityKey)) {
+            jsonBody = JsonEntity.objBuilder().putKv("type", "setting")
+                .putKv("keyLock", Integer.parseInt(abilityParam.getAbilityParam()))
+                .putKv("messageId", "CALL-" + IdUtils.generateMessageId()).build().getJsonObj();
+        }
+        else if (StringUtils.equals("settingCtl", abilityKey)) {
+            jsonBody = JsonEntity.objBuilder().putKv("type", "setting").putKv("system", abilityParam.getAbilityParam())
+                .putKv("messageId", "auto").build().getJsonObj();
+        }
+        else if (StringUtils.equals("settAutoReport", abilityKey)) {
+            JsonEntity.ObjBuilder onOffJson = JsonEntity.objBuilder();
+            onOffJson.putKv("type", "setting").putKv("messageId", "auto");
+            Integer timerInterval = Integer.parseInt(abilityParam.getAbilityParam());
+
+            if (0 == timerInterval) {
+                onOffJson.putKv("timerEnable", 0);
+            }
+            else {
+                onOffJson.putKv("timerEnable", 1);
+                onOffJson.putKv("timerInterval", timerInterval);
+            }
+
+            jsonBody = onOffJson.build().getJsonObj();
+        }
+        else if (StringUtils.equals("setOnState", abilityKey)) {
+            jsonBody = JsonEntity.objBuilder().putKv("type", "setting")
+                .putKv("onState", Integer.parseInt(abilityParam.getAbilityParam()))
+                .putKv("messageId", "CALL-" + IdUtils.generateMessageId()).build().getJsonObj();
+        }
+        else if (StringUtils.equals("triggerSync", abilityKey)) {
+            jsonBody = JsonEntity.objBuilder().putKv("type", abilityParam.getAbilityParam()).putKv("messageId", "auto")
+                .build().getJsonObj();
+        }
+
+        return jsonBody;
+    }
+
+    /**
+     * 发送MQTT消息
+     *
+     * @param topic    主题
+     * @param payload  负载数据
+     * @param qos      质量服务等级
+     * @param retained 是否保留消息
+     */
+    public void sendMqttMsg(String topic, String payload, int qos, boolean retained) {
+        log.info("[Send] Topic:{}, message:{}, qos:{}, retained:{}", topic, payload, qos, retained);
+        mqttTemplate.sendString(topic, payload, 2, false);
+    }
+
+}

+ 37 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/growatt/config/GrowattConfig.java

@@ -0,0 +1,37 @@
+/*
+ * 文 件 名:  GrowattConfig
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.growatt.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+
+/**
+ * 古瑞瓦特配置
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "adapter.growatt")
+public class GrowattConfig {
+    private String url;
+
+    private String token;
+
+    private List<String> plantList;
+}

+ 77 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/growatt/controller/GrowattController.java

@@ -0,0 +1,77 @@
+/*
+ * 文 件 名:  GrowattController
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/18
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.growatt.controller;
+
+import com.ruoyi.ems.growatt.handle.GrowattHandler;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * 古瑞瓦特光伏适配
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/18]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@RestController
+@CrossOrigin(allowedHeaders = "*", allowCredentials = "false")
+@RequestMapping("/growatt")
+@Api(value = "GrowattController", description = "古瑞瓦特-光伏适配接口")
+public class GrowattController {
+    /**
+     * 日志
+     */
+    private static final Logger log = LoggerFactory.getLogger(GrowattController.class);
+
+    @Qualifier("growattHandler")
+    @Resource
+    private GrowattHandler growattHandler;
+
+    /**
+     * 采集器能力调用
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/ct/abilityCall", method = RequestMethod.POST)
+    @ApiOperation(value = "/ct/abilityCall", notes = "采集器能力调用")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public CallResponse<Void> call(@RequestBody AbilityPayload abilityPayload) {
+        CallResponse<Void> res = null;
+
+        try {
+            res = growattHandler.call(abilityPayload);
+        }
+        catch (Exception e) {
+            log.error("call fail!", e);
+            res = new CallResponse<>(501, "内部错误:" + e.getMessage());
+        }
+
+        return res;
+    }
+}

+ 787 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/growatt/handle/GrowattHandler.java

@@ -0,0 +1,787 @@
+/*
+ * 文 件 名:  GrowattHandler
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  古瑞瓦特-光伏适配处理Handler
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/18
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  完善采集器和逆变器数据同步功能
+ */
+package com.ruoyi.ems.growatt.handle;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.huashe.common.utils.DateUtils;
+import com.ruoyi.ems.common.handle.BaseDevHandler;
+import com.ruoyi.ems.growatt.config.GrowattConfig;
+import com.ruoyi.ems.growatt.retrofit.GrowattTemplate;
+import com.ruoyi.ems.domain.EmsDevice;
+import com.ruoyi.ems.domain.EmsObjAttrValue;
+import com.ruoyi.ems.enums.DevObjType;
+import com.ruoyi.ems.enums.DevOnlineStatus;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import com.ruoyi.ems.model.QueryDevice;
+import com.ruoyi.ems.growatt.model.DataLogger;
+import com.ruoyi.ems.growatt.model.DeviceInfo;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 古瑞瓦特-光伏适配处理Handler
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/18]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Service
+public class GrowattHandler extends BaseDevHandler {
+    private static final Logger log = LoggerFactory.getLogger(GrowattHandler.class);
+
+    // 光伏系统模型代码
+    private static final String SYS_MODEL_CODE = "M_E5_SYS_PHOTOVOLTAIC";
+
+    // 光伏采集器模型代码
+    private static final String DEV_MODEL_COL = "M_W2_DEV_PHOTOVOLTAIC_COL";
+
+    // 光伏逆变器模型代码
+    private static final String DEV_MODEL_INVERTER = "M_E5_DEV_PHOTOVOLTAIC_INVERTER";
+
+    // 设备子系统代码
+    private static final String SUBSYSTEM_CODE = "SYS_GF";
+
+    // 逆变器设备类型(MAX类型)
+    private static final int INVERTER_TYPE_MAX = 18;
+
+    @Autowired
+    private GrowattConfig config;
+
+    @Override
+    public CallResponse<Void> call(AbilityPayload abilityParam) {
+        CallResponse<Void> callResponse = null;
+
+        try {
+            if (DevObjType.SYSTEM.getCode() == abilityParam.getObjType()) {
+                switch (abilityParam.getAbilityKey()) {
+                    case "SyncDevCollector":
+                        // 同步采集器数据
+                        CompletableFuture.runAsync(this::execSyncDevCollector);
+                        break;
+                    case "SyncDevInverter":
+                        // 同步逆变器列表数据
+                        CompletableFuture.runAsync(this::execSyncDevInverter);
+                        break;
+                    case "CollectInverterData":
+                        // 采集逆变器实时数据
+                        CompletableFuture.runAsync(this::execCollectInverterData);
+                        break;
+                    case "SyncAll":
+                        // 同步所有数据(采集器+逆变器+实时数据)
+                        CompletableFuture.runAsync(this::execSyncAll);
+                        break;
+                    default:
+                        throw new UnsupportedOperationException("不支持的abilityKey: " + abilityParam.getAbilityKey());
+                }
+            }
+            else if (DevObjType.DEVC.getCode() == abilityParam.getObjType()) {
+                if (StringUtils.equals(DEV_MODEL_COL, abilityParam.getModelCode())) {
+                    // 采集器设备能力
+                    switch (abilityParam.getAbilityKey()) {
+                        case "SyncAttr":
+                            execSyncCollectorAttr(abilityParam.getObjCode());
+                            break;
+                        default:
+                            throw new UnsupportedOperationException(
+                                "不支持的abilityKey: " + abilityParam.getAbilityKey());
+                    }
+                }
+                else if (StringUtils.equals(DEV_MODEL_INVERTER, abilityParam.getModelCode())) {
+                    // 逆变器设备能力
+                    switch (abilityParam.getAbilityKey()) {
+                        case "SyncAttr":
+                            execSyncInverterAttr(abilityParam.getObjCode());
+                            break;
+                        default:
+                            throw new UnsupportedOperationException(
+                                "不支持的abilityKey: " + abilityParam.getAbilityKey());
+                    }
+                }
+            }
+
+            callResponse = new CallResponse<>(0, "成功");
+        }
+        catch (Exception e) {
+            callResponse = new CallResponse<>(-1, e.getMessage());
+            log.error("调用ability异常", e);
+        }
+
+        return callResponse;
+    }
+
+    @Override
+    public List<EmsDevice> getDeviceList() {
+        QueryDevice queryDevice = new QueryDevice();
+        queryDevice.setDeviceModel(DEV_MODEL_INVERTER);
+        queryDevice.setSubsystemCode(SUBSYSTEM_CODE);
+        return deviceService.selectList(queryDevice);
+    }
+
+    @Override
+    public void refreshOnline() {
+        log.info("开始刷新光伏设备在线状态...");
+
+        try {
+            GrowattTemplate template = new GrowattTemplate(config.getUrl(), config.getToken());
+
+            // ==================== 第一步:刷新采集器在线状态 ====================
+            List<String> plantList = config.getPlantList();
+            if (CollectionUtils.isEmpty(plantList)) {
+                log.warn("未配置电站ID列表");
+                return;
+            }
+
+            // 从所有电站获取采集器列表
+            List<DataLogger> allDataLoggers = new ArrayList<>();
+            for (String plantId : plantList) {
+                List<DataLogger> dataLoggers = template.getDatalogList(plantId);
+                if (CollectionUtils.isNotEmpty(dataLoggers)) {
+                    allDataLoggers.addAll(dataLoggers);
+                }
+            }
+
+            if (CollectionUtils.isEmpty(allDataLoggers)) {
+                log.warn("API返回采集器列表为空");
+                return;
+            }
+
+            // 将API数据转为Map,key为sn
+            Map<String, DataLogger> apiDataMap = allDataLoggers.stream()
+                .collect(Collectors.toMap(DataLogger::getSn, Function.identity()));
+
+            // 查询本地采集器设备
+            QueryDevice queryCollector = new QueryDevice();
+            queryCollector.setDeviceModel(DEV_MODEL_COL);
+            queryCollector.setSubsystemCode(SUBSYSTEM_CODE);
+            List<EmsDevice> collectorDevices = deviceService.selectList(queryCollector);
+
+            // 采集器sn -> 在线状态 的映射(用于后续判断逆变器状态)
+            Map<String, Boolean> collectorOnlineMap = new HashMap<>();
+
+            for (EmsDevice device : collectorDevices) {
+                EmsObjAttrValue snAttr = objAttrValueService.selectObjAttrValue(DEV_MODEL_COL, device.getDeviceCode(),
+                    "sn");
+
+                if (snAttr == null || StringUtils.isBlank(snAttr.getAttrValue())) {
+                    continue;
+                }
+
+                String sn = snAttr.getAttrValue();
+                DataLogger apiData = apiDataMap.get(sn);
+
+                if (apiData == null) {
+                    continue;
+                }
+
+                // lost=false 表示在线,lost=true 表示离线
+                boolean isOnline = !apiData.isLost();
+                collectorOnlineMap.put(sn, isOnline);
+
+                int newStatus = isOnline ? DevOnlineStatus.ONLINE.getCode() : DevOnlineStatus.OFFLINE.getCode();
+                if (device.getDeviceStatus() == null || device.getDeviceStatus() != newStatus) {
+                    device.setDeviceStatus(newStatus);
+                    deviceService.updateEmsDevice(device);
+                    triggerEvent(device, isOnline ? "online" : "offline", null, new Date());
+                    log.info("采集器 {} 状态更新为 {}", device.getDeviceCode(), isOnline ? "在线" : "离线");
+                }
+            }
+
+            // ==================== 第二步:刷新逆变器在线状态 ====================
+            // 逆变器的在线状态由其关联的采集器决定
+            QueryDevice queryInverter = new QueryDevice();
+            queryInverter.setDeviceModel(DEV_MODEL_INVERTER);
+            queryInverter.setSubsystemCode(SUBSYSTEM_CODE);
+            List<EmsDevice> inverterDevices = deviceService.selectList(queryInverter);
+
+            for (EmsDevice device : inverterDevices) {
+                // 获取逆变器关联的采集器sn(dataloggerSn属性)
+                EmsObjAttrValue dataloggerSnAttr = objAttrValueService.selectObjAttrValue(DEV_MODEL_INVERTER,
+                    device.getDeviceCode(), "dataloggerSn");
+
+                if (dataloggerSnAttr == null || StringUtils.isBlank(dataloggerSnAttr.getAttrValue())) {
+                    continue;
+                }
+
+                String dataloggerSn = dataloggerSnAttr.getAttrValue();
+
+                // 根据关联的采集器状态判断逆变器状态
+                Boolean collectorOnline = collectorOnlineMap.get(dataloggerSn);
+                boolean isOnline = collectorOnline != null && collectorOnline;
+
+                int newStatus = isOnline ? DevOnlineStatus.ONLINE.getCode() : DevOnlineStatus.OFFLINE.getCode();
+                if (device.getDeviceStatus() == null || device.getDeviceStatus() != newStatus) {
+                    device.setDeviceStatus(newStatus);
+                    deviceService.updateEmsDevice(device);
+                    triggerEvent(device, isOnline ? "online" : "offline", null, new Date());
+                    log.info("逆变器 {} 状态更新为 {} (关联采集器:{})", device.getDeviceCode(),
+                        isOnline ? "在线" : "离线", dataloggerSn);
+                }
+            }
+
+            log.info("光伏设备在线状态刷新完成");
+
+        }
+        catch (Exception e) {
+            log.error("刷新光伏设备在线状态异常", e);
+        }
+    }
+
+    /**
+     * 同步所有数据
+     */
+    public void execSyncAll() {
+        log.info("开始同步光伏系统所有数据...");
+        execSyncDevCollector();
+        execSyncDevInverter();
+        execCollectInverterData();
+        log.info("光伏系统数据同步完成");
+    }
+
+    /**
+     * 同步采集器数据
+     * 调用API获取采集器列表,与本地数据库比对,更新状态和属性
+     */
+    public void execSyncDevCollector() {
+        log.info("开始同步采集器数据...");
+
+        try {
+            GrowattTemplate template = new GrowattTemplate(config.getUrl(), config.getToken());
+
+            // 1. 从API获取所有电站的采集器列表
+            List<DataLogger> allDataLoggers = new ArrayList<>();
+            List<String> plantList = config.getPlantList();
+            if (CollectionUtils.isEmpty(plantList)) {
+                log.warn("未配置电站ID列表");
+                return;
+            }
+
+            for (String plantId : plantList) {
+                List<DataLogger> dataLoggers = template.getDatalogList(plantId);
+                if (CollectionUtils.isNotEmpty(dataLoggers)) {
+                    allDataLoggers.addAll(dataLoggers);
+                }
+                log.info("从电站 {} 获取到 {} 个采集器", plantId, dataLoggers.size());
+            }
+
+            if (CollectionUtils.isEmpty(allDataLoggers)) {
+                log.warn("API返回采集器列表为空");
+                return;
+            }
+
+            log.info("从API共获取到 {} 个采集器", allDataLoggers.size());
+
+            // 将API数据转为Map,key为sn
+            Map<String, DataLogger> apiDataMap = allDataLoggers.stream()
+                .collect(Collectors.toMap(DataLogger::getSn, Function.identity(), (k1, k2) -> k1));
+
+            // 2. 查询本地数据库中的采集器设备
+            QueryDevice queryDevice = new QueryDevice();
+            queryDevice.setDeviceModel(DEV_MODEL_COL);
+            queryDevice.setSubsystemCode(SUBSYSTEM_CODE);
+            List<EmsDevice> localDevices = deviceService.selectList(queryDevice);
+            log.info("本地数据库有 {} 个采集器设备", localDevices.size());
+
+            if (CollectionUtils.isEmpty(localDevices)) {
+                log.warn("本地数据库无采集器设备");
+                return;
+            }
+
+            // 3. 遍历本地设备,通过sn属性与API数据匹配
+            for (EmsDevice device : localDevices) {
+                try {
+                    // 查询设备的sn属性
+                    EmsObjAttrValue snAttr = objAttrValueService.selectObjAttrValue(DEV_MODEL_COL,
+                        device.getDeviceCode(), "sn");
+
+                    if (snAttr == null || StringUtils.isBlank(snAttr.getAttrValue())) {
+                        log.warn("采集器设备 {} 无sn属性,跳过", device.getDeviceCode());
+                        continue;
+                    }
+
+                    String sn = snAttr.getAttrValue();
+                    DataLogger apiData = apiDataMap.get(sn);
+
+                    if (apiData == null) {
+                        log.warn("采集器 {} (sn={}) 在API中未找到", device.getDeviceCode(), sn);
+                        continue;
+                    }
+
+                    // 4. 更新设备属性
+                    List<EmsObjAttrValue> existingAttrs = objAttrValueService.selectByObjCode(DEV_MODEL_COL,
+                        device.getDeviceCode());
+                    Map<String, EmsObjAttrValue> attrMap = existingAttrs.stream()
+                        .collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, Function.identity()));
+
+                    // 更新或插入属性值
+                    checkAndUpdate(attrMap, device.getDeviceCode(), DEV_MODEL_COL, "manufacturer",
+                        apiData.getManufacturer());
+                    checkAndUpdate(attrMap, device.getDeviceCode(), DEV_MODEL_COL, "model", apiData.getModel());
+                    checkAndUpdate(attrMap, device.getDeviceCode(), DEV_MODEL_COL, "netMode", apiData.getNetMode());
+                    checkAndUpdate(attrMap, device.getDeviceCode(), DEV_MODEL_COL, "type",
+                        String.valueOf(apiData.getType()));
+
+                    // lastUpdateTime 需要特殊处理
+                    if (apiData.getUpdateTime() != null) {
+                        String updateTimeStr = DateUtils.dateToString(apiData.getUpdateTime(), "yyyy-MM-dd HH:mm:ss");
+                        checkAndUpdate(attrMap, device.getDeviceCode(), DEV_MODEL_COL, "lastUpdateTime", updateTimeStr);
+                    }
+
+                    log.debug("采集器 {} 属性已更新", device.getDeviceCode());
+
+                }
+                catch (Exception e) {
+                    log.error("处理采集器设备 {} 异常", device.getDeviceCode(), e);
+                }
+            }
+
+            // 记录调用日志
+            saveCallLog(SUBSYSTEM_CODE, SYS_MODEL_CODE, "SyncDevCollector", 0,
+                "{\"plantList\":\"" + StringUtils.join(plantList, ",") + "\"}",
+                "{\"count\":" + allDataLoggers.size() + "}");
+
+            log.info("采集器数据同步完成");
+
+        }
+        catch (Exception e) {
+            log.error("同步采集器数据异常", e);
+            saveCallLog(SUBSYSTEM_CODE, SYS_MODEL_CODE, "SyncDevCollector", -1,
+                "{\"plantList\":\"" + StringUtils.join(config.getPlantList(), ",") + "\"}", e.getMessage());
+        }
+    }
+
+    /**
+     * 同步逆变器列表数据
+     * 调用API获取逆变器列表,与本地数据库比对,更新状态和属性
+     */
+    public void execSyncDevInverter() {
+        log.info("开始同步逆变器列表数据...");
+
+        try {
+            GrowattTemplate template = new GrowattTemplate(config.getUrl(), config.getToken());
+
+            // 1. 从API获取所有电站的设备列表
+            List<DeviceInfo> allDeviceList = new ArrayList<>();
+            List<String> plantList = config.getPlantList();
+            if (CollectionUtils.isEmpty(plantList)) {
+                log.warn("未配置电站ID列表");
+                return;
+            }
+
+            for (String plantId : plantList) {
+                List<DeviceInfo> deviceList = template.getDeviceList(plantId);
+                if (CollectionUtils.isNotEmpty(deviceList)) {
+                    allDeviceList.addAll(deviceList);
+                }
+                log.info("从电站 {} 获取到 {} 个设备", plantId, deviceList.size());
+            }
+
+            if (CollectionUtils.isEmpty(allDeviceList)) {
+                log.warn("API返回设备列表为空");
+                return;
+            }
+
+            log.info("从API共获取到 {} 个设备", allDeviceList.size());
+
+            // API返回的设备列表包含重复记录(type=1和type=4),需要去重
+            Map<String, DeviceInfo> apiDataMap = allDeviceList.stream().filter(info -> info.getType() == 1)
+                .collect(Collectors.toMap(DeviceInfo::getDeviceSn, Function.identity(), (k1, k2) -> k1));
+            log.info("过滤type=1后有 {} 个逆变器设备", apiDataMap.size());
+
+            // 2. 查询本地数据库中的逆变器设备
+            QueryDevice queryDevice = new QueryDevice();
+            queryDevice.setDeviceModel(DEV_MODEL_INVERTER);
+            queryDevice.setSubsystemCode(SUBSYSTEM_CODE);
+            List<EmsDevice> localDevices = deviceService.selectList(queryDevice);
+            log.info("本地数据库有 {} 个逆变器设备", localDevices.size());
+
+            if (CollectionUtils.isEmpty(localDevices)) {
+                log.warn("本地数据库无逆变器设备");
+                return;
+            }
+
+            // 3. 遍历本地设备,通过sn属性与API数据匹配
+            for (EmsDevice device : localDevices) {
+                try {
+                    // 查询设备的sn属性
+                    EmsObjAttrValue snAttr = objAttrValueService.selectObjAttrValue(DEV_MODEL_INVERTER,
+                        device.getDeviceCode(), "sn");
+
+                    if (snAttr == null || StringUtils.isBlank(snAttr.getAttrValue())) {
+                        log.warn("逆变器设备 {} 无sn属性,跳过", device.getDeviceCode());
+                        continue;
+                    }
+
+                    String sn = snAttr.getAttrValue();
+                    DeviceInfo apiData = apiDataMap.get(sn);
+
+                    if (apiData == null) {
+                        log.warn("逆变器 {} (sn={}) 在API中未找到", device.getDeviceCode(), sn);
+                        continue;
+                    }
+
+                    // 4. 更新设备属性
+                    List<EmsObjAttrValue> existingAttrs = objAttrValueService.selectByObjCode(DEV_MODEL_INVERTER,
+                        device.getDeviceCode());
+                    Map<String, EmsObjAttrValue> attrMap = existingAttrs.stream()
+                        .collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, Function.identity(), (k1, k2) -> k1));
+
+                    // 更新或插入属性值
+                    checkAndUpdate(attrMap, device.getDeviceCode(), DEV_MODEL_INVERTER, "manufacturer",
+                        apiData.getManufacturer());
+                    checkAndUpdate(attrMap, device.getDeviceCode(), DEV_MODEL_INVERTER, "model", apiData.getModel());
+                    checkAndUpdate(attrMap, device.getDeviceCode(), DEV_MODEL_INVERTER, "type",
+                        String.valueOf(apiData.getType()));
+                    checkAndUpdate(attrMap, device.getDeviceCode(), DEV_MODEL_INVERTER, "status",
+                        String.valueOf(apiData.getStatus()));
+                    checkAndUpdate(attrMap, device.getDeviceCode(), DEV_MODEL_INVERTER, "lastUpdateTime",
+                        apiData.getLastUpdateTime());
+
+                    log.debug("逆变器 {} 属性已更新", device.getDeviceCode());
+
+                }
+                catch (Exception e) {
+                    log.error("处理逆变器设备 {} 异常", device.getDeviceCode(), e);
+                }
+            }
+
+            // 记录调用日志
+            saveCallLog(SUBSYSTEM_CODE, SYS_MODEL_CODE, "SyncDevInverter", 0,
+                "{\"plantList\":\"" + StringUtils.join(plantList, ",") + "\"}",
+                "{\"count\":" + apiDataMap.size() + "}");
+
+            log.info("逆变器列表数据同步完成");
+
+        }
+        catch (Exception e) {
+            log.error("同步逆变器列表数据异常", e);
+            saveCallLog(SUBSYSTEM_CODE, SYS_MODEL_CODE, "SyncDevInverter", -1,
+                "{\"plantList\":\"" + StringUtils.join(config.getPlantList(), ",") + "\"}", e.getMessage());
+        }
+    }
+
+    /**
+     * 采集逆变器实时数据
+     * 批量获取逆变器运行数据并更新到数据库
+     */
+    public void execCollectInverterData() {
+        log.info("开始采集逆变器实时数据...");
+
+        try {
+            // 1. 查询本地逆变器设备
+            QueryDevice queryDevice = new QueryDevice();
+            queryDevice.setDeviceModel(DEV_MODEL_INVERTER);
+            queryDevice.setSubsystemCode(SUBSYSTEM_CODE);
+            List<EmsDevice> localDevices = deviceService.selectList(queryDevice);
+
+            if (CollectionUtils.isEmpty(localDevices)) {
+                log.warn("本地数据库无逆变器设备");
+                return;
+            }
+
+            // 2. 获取所有逆变器的sn
+            Set<String> snSet = new HashSet<>();
+            Map<String, EmsDevice> snDeviceMap = new HashMap<>();
+
+            for (EmsDevice device : localDevices) {
+                EmsObjAttrValue snAttr = objAttrValueService.selectObjAttrValue(DEV_MODEL_INVERTER,
+                    device.getDeviceCode(), "sn");
+                if (snAttr != null && StringUtils.isNotBlank(snAttr.getAttrValue())) {
+                    snSet.add(snAttr.getAttrValue());
+                    snDeviceMap.put(snAttr.getAttrValue(), device);
+                }
+            }
+
+            if (snSet.isEmpty()) {
+                log.warn("没有可采集的逆变器sn");
+                return;
+            }
+
+            log.info("准备采集 {} 个逆变器的实时数据", snSet.size());
+
+            // 3. 调用API批量获取设备数据
+            GrowattTemplate template = new GrowattTemplate(config.getUrl(), config.getToken());
+            List<String> snList = new ArrayList<>(snSet);
+            JSONObject deviceDataJson = template.getDeviceData(INVERTER_TYPE_MAX, snList);
+
+            if (deviceDataJson == null || deviceDataJson.isEmpty()) {
+                log.warn("API返回设备数据为空");
+                return;
+            }
+
+            log.info("从API获取到 {} 个设备的实时数据", deviceDataJson.size());
+
+            // 4. 解析并更新每个设备的数据
+            deviceDataJson.forEach((sn, dataObj) -> {
+                EmsDevice device = snDeviceMap.get(sn);
+
+                if (device != null) {
+                    try {
+                        JSONObject data = (JSONObject) dataObj;
+                        updateInverterRuntimeData(device, data);
+                        log.debug("逆变器 {} 实时数据已更新", device.getDeviceCode());
+                    }
+                    catch (Exception e) {
+                        log.error("更新逆变器 {} 实时数据异常", device.getDeviceCode(), e);
+                    }
+                }
+                else {
+                    log.warn("sn={} 未找到对应设备", sn);
+                }
+            });
+
+            // 记录调用日志
+            saveCallLog(SUBSYSTEM_CODE, SYS_MODEL_CODE, "CollectInverterData", 0, "{\"snCount\":" + snSet.size() + "}",
+                "{\"dataCount\":" + deviceDataJson.size() + "}");
+
+            log.info("逆变器实时数据采集完成");
+
+        }
+        catch (Exception e) {
+            log.error("采集逆变器实时数据异常", e);
+            saveCallLog(SUBSYSTEM_CODE, SYS_MODEL_CODE, "CollectInverterData", -1, null, e.getMessage());
+        }
+    }
+
+    /**
+     * 更新逆变器运行时数据
+     *
+     * @param device 设备
+     * @param data   API返回的数据
+     */
+    private void updateInverterRuntimeData(EmsDevice device, JSONObject data) {
+        // 获取现有属性
+        List<EmsObjAttrValue> existingAttrs = objAttrValueService.selectByObjCode(DEV_MODEL_INVERTER,
+            device.getDeviceCode());
+        Map<String, EmsObjAttrValue> attrMap = existingAttrs.stream()
+            .collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, Function.identity()));
+
+        String deviceCode = device.getDeviceCode();
+
+        // 更新设备状态属性
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "statusText", data.getString("statusText"));
+
+        // === 功率相关 ===
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "pac", formatDouble(data.getDouble("pac")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "ppv", formatDouble(data.getDouble("ppv")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "ppv1", formatDouble(data.getDouble("ppv1")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "ppv2", formatDouble(data.getDouble("ppv2")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "ppv3", formatDouble(data.getDouble("ppv3")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "ppv4", formatDouble(data.getDouble("ppv4")));
+
+        // === 电压相关 (PV输入电压) ===
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "vpv1", formatDouble(data.getDouble("vpv1")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "vpv2", formatDouble(data.getDouble("vpv2")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "vpv3", formatDouble(data.getDouble("vpv3")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "vpv4", formatDouble(data.getDouble("vpv4")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "vpv5", formatDouble(data.getDouble("vpv5")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "vpv6", formatDouble(data.getDouble("vpv6")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "vpv7", formatDouble(data.getDouble("vpv7")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "vpv8", formatDouble(data.getDouble("vpv8")));
+
+        // === 组串电压 (vString) ===
+        for (int i = 1; i <= 16; i++) {
+            String key = "vString" + i;
+            Double value = data.getDouble(key);
+            if (value != null && value != 0.0) {
+                checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, key, formatDouble(value));
+            }
+        }
+
+        // === 母线电压 ===
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "pBusVoltage",
+            formatDouble(data.getDouble("pBusVoltage")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "nBusVoltage",
+            formatDouble(data.getDouble("nBusVoltage")));
+
+        // === 交流输出电压 ===
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "vacr", formatDouble(data.getDouble("vacr")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "vacs", formatDouble(data.getDouble("vacs")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "vact", formatDouble(data.getDouble("vact")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "vacRs", formatDouble(data.getDouble("vacRs")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "vacSt", formatDouble(data.getDouble("vacSt")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "vacTr", formatDouble(data.getDouble("vacTr")));
+
+        // === 温度相关 ===
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "temperature",
+            formatDouble(data.getDouble("temperature")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "temperature2",
+            formatDouble(data.getDouble("temperature2")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "temperature3",
+            formatDouble(data.getDouble("temperature3")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "temperature5",
+            formatDouble(data.getDouble("temperature5")));
+
+        // === 发电量相关 ===
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "eacToday", formatDouble(data.getDouble("eacToday")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "eacTotal", formatDouble(data.getDouble("eacTotal")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "epvTotal", formatDouble(data.getDouble("epvTotal")));
+
+        // === 频率 ===
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "fac", formatDouble(data.getDouble("fac")));
+
+        // === 功率因数 ===
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "pf", formatDouble(data.getDouble("pf")));
+
+        // === 故障相关 ===
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "faultCode1",
+            String.valueOf(data.getIntValue("faultCode1")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "faultCode2",
+            String.valueOf(data.getIntValue("faultCode2")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "faultType",
+            String.valueOf(data.getIntValue("faultType")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "faultValue",
+            String.valueOf(data.getIntValue("faultValue")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "warnCode",
+            String.valueOf(data.getIntValue("warnCode")));
+
+        // === 其他运行参数 ===
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "deratingMode",
+            String.valueOf(data.getIntValue("deratingMode")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "gfci", String.valueOf(data.getIntValue("gfci")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "pvIso", formatDouble(data.getDouble("pvIso")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "pidBus", formatDouble(data.getDouble("pidBus")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "opFullwatt",
+            formatDouble(data.getDouble("opFullwatt")));
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "timeTotal", formatDouble(data.getDouble("timeTotal")));
+
+        // === 更新时间 ===
+        checkAndUpdate(attrMap, deviceCode, DEV_MODEL_INVERTER, "time", data.getString("time"));
+    }
+
+    /**
+     * 格式化Double值,保留2位小数
+     */
+    private String formatDouble(Double value) {
+        if (value == null) {
+            return "0";
+        }
+        return String.format("%.2f", value);
+    }
+
+    /**
+     * 同步单个采集器属性
+     */
+    private void execSyncCollectorAttr(String objCode) {
+        log.info("同步采集器 {} 属性", objCode);
+
+        try {
+            // 获取设备sn
+            EmsObjAttrValue snAttr = objAttrValueService.selectObjAttrValue(DEV_MODEL_COL, objCode, "sn");
+            if (snAttr == null || StringUtils.isBlank(snAttr.getAttrValue())) {
+                log.warn("采集器 {} 无sn属性", objCode);
+                return;
+            }
+
+            GrowattTemplate template = new GrowattTemplate(config.getUrl(), config.getToken());
+
+            // 从所有电站获取采集器列表
+            List<DataLogger> allDataLoggers = new ArrayList<>();
+            List<String> plantList = config.getPlantList();
+            for (String plantId : plantList) {
+                List<DataLogger> dataLoggers = template.getDatalogList(plantId);
+                if (CollectionUtils.isNotEmpty(dataLoggers)) {
+                    allDataLoggers.addAll(dataLoggers);
+                }
+            }
+
+            String sn = snAttr.getAttrValue();
+            DataLogger target = allDataLoggers.stream().filter(d -> sn.equals(d.getSn())).findFirst().orElse(null);
+
+            if (target == null) {
+                log.warn("采集器 sn={} 在API中未找到", sn);
+                return;
+            }
+
+            // 更新属性
+            List<EmsObjAttrValue> existingAttrs = objAttrValueService.selectByObjCode(DEV_MODEL_COL, objCode);
+            Map<String, EmsObjAttrValue> attrMap = existingAttrs.stream()
+                .collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, Function.identity(), (k1, k2) -> k1));
+
+            checkAndUpdate(attrMap, objCode, DEV_MODEL_COL, "manufacturer", target.getManufacturer());
+            checkAndUpdate(attrMap, objCode, DEV_MODEL_COL, "model", target.getModel());
+            checkAndUpdate(attrMap, objCode, DEV_MODEL_COL, "netMode", target.getNetMode());
+            checkAndUpdate(attrMap, objCode, DEV_MODEL_COL, "type", String.valueOf(target.getType()));
+
+            log.info("采集器 {} 属性同步完成", objCode);
+
+        }
+        catch (Exception e) {
+            log.error("同步采集器 {} 属性异常", objCode, e);
+        }
+    }
+
+    /**
+     * 同步单个逆变器属性
+     */
+    private void execSyncInverterAttr(String objCode) {
+        log.info("同步逆变器 {} 属性", objCode);
+
+        try {
+            // 获取设备sn
+            EmsObjAttrValue snAttr = objAttrValueService.selectObjAttrValue(DEV_MODEL_INVERTER, objCode, "sn");
+            if (snAttr == null || StringUtils.isBlank(snAttr.getAttrValue())) {
+                log.warn("逆变器 {} 无sn属性", objCode);
+                return;
+            }
+
+            String sn = snAttr.getAttrValue();
+
+            // 调用API获取实时数据
+            GrowattTemplate template = new GrowattTemplate(config.getUrl(), config.getToken());
+            Map<String, Object> deviceDataMap = template.getDeviceData(INVERTER_TYPE_MAX,
+                Collections.singletonList(sn));
+
+            if (deviceDataMap == null || !deviceDataMap.containsKey(sn)) {
+                log.warn("逆变器 sn={} 在API中未找到实时数据", sn);
+                return;
+            }
+
+            // 查询设备
+            QueryDevice query = new QueryDevice();
+            query.setDeviceModel(DEV_MODEL_INVERTER);
+            query.setDeviceCode(objCode);
+            List<EmsDevice> devices = deviceService.selectList(query);
+
+            if (CollectionUtils.isEmpty(devices)) {
+                log.warn("逆变器设备 {} 不存在", objCode);
+                return;
+            }
+
+            JSONObject data = (JSONObject) deviceDataMap.get(sn);
+            updateInverterRuntimeData(devices.get(0), data);
+
+            log.info("逆变器 {} 属性同步完成", objCode);
+
+        }
+        catch (Exception e) {
+            log.error("同步逆变器 {} 属性异常", objCode, e);
+        }
+    }
+}

+ 43 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/growatt/model/DataLogger.java

@@ -0,0 +1,43 @@
+/*
+ * 文 件 名:  DataLogger
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/16
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.growatt.model;
+
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 古瑞瓦特采集器
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/16]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class DataLogger {
+    private String sn;
+    private String model;
+    private Integer type;
+    @JSONField(name = "netmode")
+    private String netMode;
+    private String manufacturer;
+    private boolean lost;
+    @JSONField(name = "last_update_time")
+    private JSONObject updateTime;
+
+    public Date getUpdateTime() {
+        return new Date(updateTime.getLongValue("time"));
+    }
+}

+ 51 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/growatt/model/DeviceInfo.java

@@ -0,0 +1,51 @@
+/*
+ * 文 件 名:  DeviceInfo
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/16
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.growatt.model;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * 设备(逆变器)
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/16]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class DeviceInfo {
+    @JSONField(name = "device_sn")
+    private String deviceSn;
+
+    private String model;
+
+    /**
+     * 设备类型(1:逆变器(含MAX),2:储能机,3:其他设备,4:MAX(单MAX),5:SPH,6:SPA,7:MIN,8:PCS,9:HPS,10:PBD)
+     */
+    private int type;
+
+    @JSONField(name = "datalogger_sn")
+    private String dataLoggerSn;
+
+    /**
+     * 	设备状态
+     */
+    private int status;
+
+    private String manufacturer;
+
+    private boolean lost;
+
+    @JSONField(name = "last_update_time")
+    private String lastUpdateTime;
+}

+ 66 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/growatt/retrofit/GrowattApi.java

@@ -0,0 +1,66 @@
+/*
+ * 文 件 名:  GrowattApi
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.growatt.retrofit;
+
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Header;
+import retrofit2.http.Headers;
+import retrofit2.http.POST;
+import retrofit2.http.Query;
+
+/**
+ * Growatt - 古瑞瓦特(逆变器、光伏监测API)
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+public interface GrowattApi {
+    /**
+     * 获取采集器列表
+     *
+     * @param token   认证令牌
+     * @param plantId 电站ID
+     * @return 采集器列表
+     */
+    @Headers({ "Content-Type: application/json" })
+    @GET("/v1/device/datalogger/list")
+    Call<String> getDatalogList(@Header("token") String token, @Query("plant_id") String plantId,
+        @Query("page") Integer pageNo, @Query("perpage") Integer pageSize);
+
+    /**
+     * 获取电站设备列表
+     *
+     * @param token   认证令牌
+     * @param plantId 电站ID
+     * @return 设备列表
+     */
+    @Headers({ "Content-Type: application/json" })
+    @GET("/v1/device/list")
+    Call<String> getDeviceList(@Header("token") String token, @Query("plant_id") String plantId,
+        @Query("page") Integer pageNo, @Query("perpage") Integer pageSize);
+
+    /**
+     * 获取设备实时数据
+     *
+     * @param token   认证令牌
+     * @param type 类型 16:inverter, 17:SPH, 18:MAX, 19:SPA, 22:MIN
+     * @param devices 设备ID
+     * @return 设备列表
+     */
+    @Headers({ "Content-Type: application/json" })
+    @POST("/v1/device/last/datas")
+    Call<String> getDeviceData(@Header("token") String token, @Query("type") int type,
+        @Query("devices") String devices);
+}

+ 213 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/growatt/retrofit/GrowattTemplate.java

@@ -0,0 +1,213 @@
+/*
+ * 文 件 名:  GrowattTemplate
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/16
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.growatt.retrofit;
+
+import com.alibaba.fastjson.support.retrofit.Retrofit2ConverterFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.huashe.common.exception.Assert;
+import com.ruoyi.ems.common.retrofit.BaseApiTemplate;
+import com.ruoyi.ems.growatt.model.DataLogger;
+import com.ruoyi.ems.growatt.model.DeviceInfo;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.OkHttpClient;
+import org.apache.commons.lang3.StringUtils;
+import retrofit2.Call;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Growatt - 古瑞瓦特 操作模板
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/16]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Slf4j
+public class GrowattTemplate extends BaseApiTemplate {
+    /**
+     * 访问地址
+     */
+    protected String url;
+
+    /**
+     * 调用令牌
+     */
+    protected String token;
+
+    /**
+     * 调用代理
+     */
+    protected final GrowattApi api;
+
+    public GrowattTemplate(String restUrl, String token) {
+        this.url = restUrl;
+        this.token = token;
+
+        OkHttpClient httpClient = getClient();
+        Retrofit retrofit = new Retrofit.Builder().baseUrl(restUrl)
+            .addConverterFactory(Retrofit2ConverterFactory.create()).client(httpClient).build();
+        this.api = retrofit.create(GrowattApi.class);
+    }
+
+    /**
+     * 构造调用模板
+     *
+     * @param restUrl        服务地址 (http://ip:port)
+     * @param connectTimeout 连接超时
+     * @param readTimeout    读取超时
+     * @param writeTimeout   写超时
+     */
+    public GrowattTemplate(String restUrl, String token, int connectTimeout, int readTimeout, int writeTimeout) {
+        super.connectTimeout = connectTimeout;
+        super.readTimeout = readTimeout;
+        super.writeTimeout = writeTimeout;
+        this.url = restUrl;
+        this.token = token;
+
+        OkHttpClient httpClient = getClient();
+        Retrofit retrofit = new Retrofit.Builder().baseUrl(restUrl)
+            .addConverterFactory(Retrofit2ConverterFactory.create()).client(httpClient).build();
+        api = retrofit.create(GrowattApi.class);
+    }
+
+    /**
+     * 获取采集器列表
+     *
+     * @param plantId 电站ID
+     * @return 采集器列表
+     */
+    public List<DataLogger> getDatalogList(String plantId) {
+        List<DataLogger> list = new ArrayList<>();
+
+        try {
+            int pageNo = 1;
+            int pageSize = 100;
+            boolean isLastPage = true;
+
+            do {
+                // 执行调用
+                Call<String> call = api.getDatalogList(token, plantId, pageNo, pageSize);
+                Response<String> response = call.execute();
+                log.debug("getDatalogList response:{}", response);
+
+                Assert.isTrue(response.isSuccessful(), response.code(), response.message());
+                JSONObject resJson = JSONObject.parseObject(response.body());
+                Assert.isTrue(resJson.getIntValue("error_code") == 0, resJson.getIntValue("error_code"),
+                    resJson.getString("error_msg"));
+
+                JSONObject data = resJson.getJSONObject("data");
+
+                if (null != data) {
+                    String dataStr = data.getString("dataloggers");
+                    list.addAll(JSON.parseArray(dataStr, DataLogger.class));
+
+                    int count = data.getIntValue("count");
+                    isLastPage = list.size() >= count;
+
+                    if (!isLastPage) {
+                        pageNo++;
+                    }
+                }
+            }
+            while (!isLastPage);
+        }
+        catch (Exception e) {
+            log.error("getDatalogList fail!", e);
+        }
+
+        return list;
+    }
+
+    /**
+     * 获取设备列表
+     *
+     * @param plantId 电站ID
+     * @return 设备列表
+     */
+    public List<DeviceInfo> getDeviceList(String plantId) {
+        List<DeviceInfo> list = new ArrayList<>();
+
+        try {
+            int pageNo = 1;
+            int pageSize = 100;
+            boolean isLastPage = true;
+
+            do {
+                // 执行调用
+                Call<String> call = api.getDeviceList(token, plantId, pageNo, pageSize);
+                Response<String> response = call.execute();
+                log.debug("getDeviceList response:{}", response);
+
+                Assert.isTrue(response.isSuccessful(), response.code(), response.message());
+                JSONObject resJson = JSONObject.parseObject(response.body());
+                Assert.isTrue(resJson.getIntValue("error_code") == 0, resJson.getIntValue("error_code"),
+                    resJson.getString("error_msg"));
+
+                JSONObject data = resJson.getJSONObject("data");
+
+                if (null != data) {
+                    String dataStr = data.getString("devices");
+                    list.addAll(JSON.parseArray(dataStr, DeviceInfo.class));
+
+                    int count = data.getIntValue("count");
+                    isLastPage = list.size() >= count;
+
+                    if (!isLastPage) {
+                        pageNo++;
+                    }
+                }
+            }
+            while (!isLastPage);
+        }
+        catch (Exception e) {
+            log.error("getDeviceList fail!", e);
+        }
+
+        return list;
+    }
+
+    /**
+     * 获取设备列表
+     *
+     * @param type 类型 16:inverter, 17:SPH, 18:MAX, 19:SPA, 22:MIN
+     * @param snList 设备SN集合
+     * @return 设备列表
+     */
+    public JSONObject getDeviceData(int type, List<String> snList) {
+        JSONObject retJson = null;
+
+        try {
+            // 执行调用
+            String devSnStr = StringUtils.join(snList, ",");
+            Call<String> call = api.getDeviceData(token, type, devSnStr);
+            Response<String> response = call.execute();
+            log.debug("getDeviceData response:{}", response);
+
+            Assert.isTrue(response.isSuccessful(), response.code(), response.message());
+            JSONObject resJson = JSONObject.parseObject(response.body());
+            Assert.isTrue(resJson.getIntValue("error_code") == 0, resJson.getIntValue("error_code"),
+                resJson.getString("error_msg"));
+
+            retJson = resJson.getJSONObject("data");
+        }
+        catch (Exception e) {
+            log.error("getDeviceData fail!", e);
+        }
+
+        return retJson;
+    }
+}

+ 83 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/keka/controller/ButtonSwitchController.java

@@ -0,0 +1,83 @@
+/*
+ * 文 件 名:  ButtonSwitchController
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/4/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.keka.controller;
+
+import com.huashe.common.exception.BusinessException;
+import com.ruoyi.ems.common.handle.BaseDevHandler;
+import com.ruoyi.ems.geekopen.controller.CircuitBreakerController;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * 按钮开关接口
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/4/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@RestController
+@CrossOrigin(allowedHeaders = "*", allowCredentials = "false")
+@RequestMapping("/button-switch")
+@Api(value = "ButtonSwitchController", description = "按钮开关控制接口")
+public class ButtonSwitchController {
+    /**
+     * 日志
+     */
+    private static final Logger log = LoggerFactory.getLogger(CircuitBreakerController.class);
+
+    @Qualifier("keka86BsHandler")
+    @Resource
+    private BaseDevHandler keka86BsHandler;
+
+    /**
+     * KEKA 86型开关能力调用
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/keka/86ButtonSwitchCall", method = RequestMethod.POST)
+    @ApiOperation(value = "/keka/86ButtonSwitchCall", notes = "KEKA 86型按钮开关能力调用")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public CallResponse<Void> keka86ButtonSwitchCall(@RequestBody AbilityPayload abilityPayload) {
+        CallResponse<Void> res = null;
+
+        try {
+            res = keka86BsHandler.call(abilityPayload);
+        }
+        catch (BusinessException e) {
+            log.warn(e.getMessage());
+            res = new CallResponse<>(501, e.getMessage());
+        }
+        catch (Exception e) {
+            log.error("keka86ButtonSwitchCall fail!", e);
+            res = new CallResponse<>(501, "内部错误:" + e.getMessage());
+        }
+
+        return res;
+    }
+}

+ 615 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/keka/handle/Keka86BsHandler.java

@@ -0,0 +1,615 @@
+/*
+ * 文 件 名:  Keka86BsHandler
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/4/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.keka.handle;
+
+import com.huashe.common.exception.Assert;
+import com.ruoyi.ems.common.handle.BaseDevHandler;
+import com.ruoyi.ems.common.config.EmsConfig;
+import com.ruoyi.ems.common.retrofit.EmsApiTemplate;
+import com.ruoyi.ems.common.mqtt.MqttTemplate;
+import com.ruoyi.ems.domain.EmsDevice;
+import com.ruoyi.ems.domain.EmsObjAttrValue;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import com.ruoyi.ems.keka.model.ModbusCommand;
+import com.ruoyi.ems.model.QueryDevice;
+import com.ruoyi.ems.keka.model.DeviceStatus;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * 86开关对接处理
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/4/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Service
+public class Keka86BsHandler extends BaseDevHandler {
+    private static final Logger log = LoggerFactory.getLogger(Keka86BsHandler.class);
+
+    @Resource
+    @Qualifier("mqttTemplate")
+    protected MqttTemplate mqttTemplate;
+
+    @Autowired
+    private EmsConfig emsConfig;
+
+    private static final String TOPIC_PREFIX = "/sc/dtu/ctl/";
+    private static final String MODE_CODE = "M_W2_QS_KEKA_86";
+
+    private static final byte FUNC_READ = 0x03;
+    private static final byte FUNC_WRITE = 0x06;
+    private static final byte DEVICE_ADDR = 0x01;
+
+    // 单按键控制寄存器基地址 (协议: 44129=0x1021)
+    private static final int LIGHT_BASE_ADDR = 0x1021;
+
+    private static final int VALUE_ON = 0x0001;   // 闭合/开
+    private static final int VALUE_OFF = 0x0000;  // 断开/关
+
+    // 修正:读取单个寄存器只需要1个
+    private static final int READ_COUNT = 0x0001;
+
+    // ========== 新增:用于追踪待处理的读取请求 ==========
+    // Key: gatewayId, Value: 最近请求的buttonId
+    private final Map<String, Integer> pendingReadRequests = new ConcurrentHashMap<>();
+
+    @Override
+    public CallResponse<Void> call(AbilityPayload abilityParam) {
+        CallResponse<Void> callResponse = null;
+
+        List<EmsObjAttrValue> attrList = objAttrValueService.selectByObjCode(MODE_CODE, abilityParam.getObjCode());
+        Assert.notEmpty(attrList, -1, "设备协议属性未配置!");
+        Map<String, String> attrMap = attrList.stream()
+            .collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, EmsObjAttrValue::getAttrValue));
+
+        String gatewayId = attrMap.get("gatewayId");
+        String buttonId = attrMap.get("buttonId");
+
+        if (StringUtils.equals("on-off", abilityParam.getAbilityKey())) {
+            ModbusCommand command = buildControlCommand(Integer.parseInt(buttonId),
+                Integer.parseInt(abilityParam.getAbilityParam()));
+            String topic = TOPIC_PREFIX + gatewayId;
+            sendMqttHex(topic, command);
+            saveCallLog(abilityParam, command.getCommandHex(), System.currentTimeMillis(), 0);
+            callResponse = new CallResponse<>(0, "执行成功!");
+        }
+        else if (StringUtils.equals("syncState", abilityParam.getAbilityKey())) {
+            int btnId = Integer.parseInt(buttonId);
+            ModbusCommand command = buildReadCommand(btnId);
+            String topic = TOPIC_PREFIX + gatewayId;
+
+            // 记录待处理的读取请求
+            pendingReadRequests.put(gatewayId + "_" + getRegisterAddr(btnId), btnId);
+
+            sendMqttHex(topic, command);
+            saveCallLog(abilityParam, command.getCommandHex(), System.currentTimeMillis(), 0);
+            callResponse = new CallResponse<>(0, "执行成功!");
+        }
+        else {
+            callResponse = new CallResponse<>(-1, "不支持的能力键!");
+        }
+
+        return callResponse;
+    }
+
+    @Override
+    public List<EmsDevice> getDeviceList() {
+        QueryDevice queryDevice = new QueryDevice();
+        queryDevice.setDeviceModel(MODE_CODE);
+        return deviceService.selectList(queryDevice);
+    }
+
+    @Override
+    public void refreshOnline() {
+    }
+
+    @Async("msgHandleExecutor")
+    public void msgHandle(String gatewayId, String payload) {
+        try {
+            log.info("[Keka86] 网关:{}, 收到消息:{}", gatewayId, payload);
+
+            String[] hexBytes = payload.trim().split("\\s+");
+            if (hexBytes.length < 5) {
+                log.warn("[Keka86] 消息长度不足,忽略: {}", payload);
+                return;
+            }
+
+            int funcCode = Integer.parseInt(hexBytes[1].toUpperCase(), 16);
+
+            if (funcCode == 0x03) {
+                handleReadResponse(gatewayId, payload);
+            }
+            else if (funcCode == 0x06) {
+                handleWriteResponse(gatewayId, payload);
+            }
+            else if ((funcCode & 0x80) != 0) {
+                byte originalFuncCode = (byte) (funcCode & 0x7F);
+                handleErrorResponse(gatewayId, payload, originalFuncCode);
+            }
+            else {
+                log.warn("[Keka86] 未知功能码: 0x%02X, 消息: {}", funcCode, payload);
+            }
+
+        }
+        catch (Exception e) {
+            log.error("[Keka86] 网关:{}, 消息处理异常", gatewayId, e);
+        }
+    }
+
+    private String getDeviceCode(String gatewayId, int buttonId) {
+        List<EmsObjAttrValue> list = objAttrValueService
+            .selectByAttrKeyValue(MODE_CODE, "gatewayId", gatewayId);
+
+        if (CollectionUtils.isNotEmpty(list)) {
+            for (EmsObjAttrValue gatewayAttr : list) {
+                EmsObjAttrValue buttonAttr = objAttrValueService
+                    .selectObjAttrValue(MODE_CODE, gatewayAttr.getObjCode(), "buttonId");
+
+                if (buttonAttr != null
+                    && StringUtils.equals(buttonAttr.getAttrValue(), String.valueOf(buttonId))) {
+                    return gatewayAttr.getObjCode();
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 生成控制指令 (开/关)
+     */
+    public ModbusCommand buildControlCommand(int buttonId, int state) {
+        if (buttonId < 1 || buttonId > 6) {
+            throw new IllegalArgumentException("按键ID必须是1-6");
+        }
+        if (state != 0 && state != 1) {
+            throw new IllegalArgumentException("状态必须是0(关)或1(开)");
+        }
+
+        int registerAddr = getRegisterAddr(buttonId);
+        int value = (state == 1) ? VALUE_ON : VALUE_OFF;
+
+        byte[] frame = new byte[6];
+        frame[0] = DEVICE_ADDR;
+        frame[1] = FUNC_WRITE;
+        frame[2] = (byte) (registerAddr >> 8);
+        frame[3] = (byte) (registerAddr & 0xFF);
+        frame[4] = (byte) (value >> 8);
+        frame[5] = (byte) (value & 0xFF);
+
+        int crc = calculateCRC16(frame);
+        byte[] fullFrame = new byte[8];
+        System.arraycopy(frame, 0, fullFrame, 0, 6);
+        fullFrame[6] = (byte) (crc & 0xFF);
+        fullFrame[7] = (byte) (crc >> 8);
+
+        String hexString = bytesToHexString(fullFrame);
+        log.debug("[Keka86] 构建控制指令: 按键{}, 状态{}, 寄存器0x{}, 指令:{}",
+            buttonId, state == 1 ? "开" : "关", String.format("%04X", registerAddr), hexString);
+        return new ModbusCommand(fullFrame, hexString);
+    }
+
+    /**
+     * 生成读取指令
+     * 读取单个按键状态,寄存器数量=1
+     */
+    public ModbusCommand buildReadCommand(int buttonId) {
+        if (buttonId < 1 || buttonId > 6) {
+            throw new IllegalArgumentException("按键ID必须是1-6");
+        }
+
+        int registerAddr = getRegisterAddr(buttonId);
+
+        byte[] frame = new byte[6];
+        frame[0] = DEVICE_ADDR;
+        frame[1] = FUNC_READ;
+        frame[2] = (byte) (registerAddr >> 8);
+        frame[3] = (byte) (registerAddr & 0xFF);
+        frame[4] = (byte) (READ_COUNT >> 8);    // 0x00
+        frame[5] = (byte) (READ_COUNT & 0xFF);  // 0x01 (读1个寄存器)
+
+        int crc = calculateCRC16(frame);
+        byte[] fullFrame = new byte[8];
+        System.arraycopy(frame, 0, fullFrame, 0, 6);
+        fullFrame[6] = (byte) (crc & 0xFF);
+        fullFrame[7] = (byte) (crc >> 8);
+
+        String hexString = bytesToHexString(fullFrame);
+        log.debug("[Keka86] 构建读取指令: 按键{}, 寄存器0x{}, 指令:{}",
+            buttonId, String.format("%04X", registerAddr), hexString);
+        return new ModbusCommand(fullFrame, hexString);
+    }
+
+    /**
+     * 解析读取响应
+     * 响应格式: [设备地址][功能码][字节数][数据...][CRC_L][CRC_H]
+     * 根据协议文档2.1节:
+     * - 字节1: 设备地址
+     * - 字节2: 03H (功能码)
+     * - 字节3: 返回数据字节个数
+     * - 字节4-5: 返回第一个寄存器数据高字节+低字节
+     * - 字节6-7: 返回第二个寄存器数据高字节+低字节
+     * - 字节8-9: CRC低+CRC高
+     *
+     * @param hexResponse 十六进制响应字符串
+     * @param lightId     灯ID (用于返回完整状态信息)
+     * @return DeviceStatus 设备状态对象
+     */
+    public DeviceStatus parseReadResponse(String hexResponse, int lightId) {
+        byte[] response = hexStringToBytes(hexResponse);
+
+        if (response.length < 7) {
+            throw new IllegalArgumentException("响应数据长度不足");
+        }
+
+        // 验证CRC
+        byte[] dataWithoutCrc = new byte[response.length - 2];
+        System.arraycopy(response, 0, dataWithoutCrc, 0, response.length - 2);
+        int calculatedCrc = calculateCRC16(dataWithoutCrc);
+        int receivedCrc = (response[response.length - 2] & 0xFF) | ((response[response.length - 1] & 0xFF) << 8);
+
+        if (calculatedCrc != receivedCrc) {
+            throw new IllegalArgumentException(
+                String.format("CRC校验失败: 计算值=0x%04X, 接收值=0x%04X", calculatedCrc, receivedCrc));
+        }
+
+        // 解析数据
+        byte deviceAddr = response[0];
+        byte funcCode = response[1];
+        byte byteCount = response[2];
+
+        if (funcCode != FUNC_READ) {
+            // 检查是否是错误响应 (功能码最高位为1表示错误)
+            if ((funcCode & 0x80) != 0) {
+                byte errorCode = response[2];
+                String errorMsg = getModbusErrorMessage(errorCode);
+                throw new IllegalArgumentException("Modbus错误响应: " + errorMsg);
+            }
+            throw new IllegalArgumentException("功能码错误: 0x" + String.format("%02X", funcCode & 0xFF));
+        }
+
+        // 提取状态数据 (第一个寄存器的值)
+        int value = ((response[3] & 0xFF) << 8) | (response[4] & 0xFF);
+
+        // 解析状态: 0x0001=开, 0x0000=关
+        int status = (value == VALUE_ON) ? 1 : 0;
+
+        return new DeviceStatus(deviceAddr & 0xFF, lightId, status, hexResponse);
+    }
+
+    /**
+     * 获取按键对应的寄存器地址
+     */
+    private int getRegisterAddr(int buttonId) {
+        return LIGHT_BASE_ADDR + (buttonId - 1);
+    }
+
+    /**
+     * 根据寄存器地址反推按键ID
+     */
+    private int getButtonIdFromAddr(int registerAddr) {
+        return registerAddr - LIGHT_BASE_ADDR + 1;
+    }
+
+    /**
+     * 获取Modbus错误信息
+     */
+    private static String getModbusErrorMessage(byte errorCode) {
+        switch (errorCode) {
+            case 0x01:
+                return "01H - 功能码错误";
+            case 0x02:
+                return "02H - 非法数据地址";
+            case 0x03:
+                return "03H - 非法数据值";
+            case 0x04:
+                return "04H - 设备故障";
+            case 0x05:
+                return "05H - 查询帧正确,但设备正在处理";
+            case 0x06:
+                return "06H - 设备忙";
+            default:
+                return String.format("未知错误码: 0x%02X", errorCode & 0xFF);
+        }
+    }
+
+    /**
+     * 计算Modbus CRC-16
+     * 多项式: 0xA001 (反向)
+     */
+    private static int calculateCRC16(byte[] data) {
+        int crc = 0xFFFF;
+        for (byte b : data) {
+            crc ^= (b & 0xFF);
+            for (int i = 0; i < 8; i++) {
+                if ((crc & 0x0001) != 0) {
+                    crc >>= 1;
+                    crc ^= 0xA001;
+                }
+                else {
+                    crc >>= 1;
+                }
+            }
+        }
+        return crc;
+    }
+
+    /**
+     * 字节数组转十六进制字符串
+     */
+    private static String bytesToHexString(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < bytes.length; i++) {
+            if (i > 0)
+                sb.append(" ");
+            sb.append(String.format("%02X", bytes[i] & 0xFF));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 十六进制字符串转字节数组
+     */
+    private static byte[] hexStringToBytes(String hexString) {
+        String[] hexArray = hexString.trim().split("\\s+");
+        byte[] bytes = new byte[hexArray.length];
+        for (int i = 0; i < hexArray.length; i++) {
+            bytes[i] = (byte) Integer.parseInt(hexArray[i], 16);
+        }
+        return bytes;
+    }
+
+    /**
+     * 发送MQTT消息 - 优化版本
+     *
+     * @param topic   主题
+     * @param command Modbus指令对象 (包含字节数组和十六进制字符串)
+     */
+    public void sendMqttHex(String topic, ModbusCommand command) {
+        log.info("[Send] Topic:{}, message:{}, qos:{}, retained:{}", topic, command.getCommandHex(), 2, false);
+        mqttTemplate.sendHex(topic, command.getCommandBytes(), 2, false);
+    }
+
+    /**
+     * 处理读取响应 (功能码 03H)
+     *
+     * 响应格式 (读取1个寄存器):
+     * [0] 设备地址: 01
+     * [1] 功能码:   03
+     * [2] 字节数:   02 (1个寄存器=2字节)
+     * [3] 数据高:   00
+     * [4] 数据低:   01 或 00
+     * [5] CRC低
+     * [6] CRC高
+     *
+     * 示例: 01 03 02 00 01 79 84 表示状态=开
+     *       01 03 02 00 00 B8 44 表示状态=关
+     */
+    private void handleReadResponse(String gatewayId, String hexMessage) {
+        try {
+            byte[] response = hexStringToBytes(hexMessage);
+
+            // 最小长度: 地址(1) + 功能码(1) + 字节数(1) + 数据(2) + CRC(2) = 7
+            if (response.length < 7) {
+                log.warn("[Keka86-Read] 响应长度不足: {}", hexMessage);
+                return;
+            }
+
+            // CRC校验
+            byte[] dataWithoutCrc = new byte[response.length - 2];
+            System.arraycopy(response, 0, dataWithoutCrc, 0, response.length - 2);
+            int calculatedCrc = calculateCRC16(dataWithoutCrc);
+            int receivedCrc = (response[response.length - 2] & 0xFF)
+                | ((response[response.length - 1] & 0xFF) << 8);
+
+            if (calculatedCrc != receivedCrc) {
+                log.error("[Keka86-Read] CRC校验失败, 计算:0x{}, 接收:0x{}, Hex:{}",
+                    String.format("%04X", calculatedCrc),
+                    String.format("%04X", receivedCrc), hexMessage);
+                return;
+            }
+
+            // 解析响应数据
+            int deviceAddr = response[0] & 0xFF;
+            int funcCode = response[1] & 0xFF;
+            int byteCount = response[2] & 0xFF;
+
+            if (funcCode != 0x03) {
+                log.warn("[Keka86-Read] 功能码不匹配: 0x{}", String.format("%02X", funcCode));
+                return;
+            }
+
+            // 提取寄存器值 (2字节)
+            int dataValue = ((response[3] & 0xFF) << 8) | (response[4] & 0xFF);
+            int status = (dataValue == VALUE_ON) ? 1 : 0;
+
+            log.info("[Keka86-Read] 网关:{}, 设备地址:{}, 字节数:{}, 数据值:0x{}, 状态:{}",
+                gatewayId, deviceAddr, byteCount,
+                String.format("%04X", dataValue), status == 1 ? "开" : "关");
+
+            // ========== 关键修复:确定是哪个按键的响应 ==========
+            // 方案1: 从pendingReadRequests中查找 (需要记录发送时的按键)
+            // 方案2: 遍历所有按键配置,更新匹配的设备
+
+            // 这里采用方案2: 查找该网关下所有按键设备并更新
+            // 实际生产中建议用方案1,通过请求-响应匹配
+
+            List<EmsObjAttrValue> gatewayDevices = objAttrValueService
+                .selectByAttrKeyValue(MODE_CODE, "gatewayId", gatewayId);
+
+            if (CollectionUtils.isEmpty(gatewayDevices)) {
+                log.warn("[Keka86-Read] 网关:{} 无注册设备", gatewayId);
+                return;
+            }
+
+            // 检查是否有匹配的待处理请求
+            for (EmsObjAttrValue gw : gatewayDevices) {
+                String deviceCode = gw.getObjCode();
+                EmsObjAttrValue btnAttr = objAttrValueService
+                    .selectObjAttrValue(MODE_CODE, deviceCode, "buttonId");
+
+                if (btnAttr != null) {
+                    int buttonId = Integer.parseInt(btnAttr.getAttrValue());
+                    int registerAddr = getRegisterAddr(buttonId);
+                    String requestKey = gatewayId + "_" + registerAddr;
+
+                    // 检查是否是该按键的响应
+                    if (pendingReadRequests.containsKey(requestKey)) {
+                        pendingReadRequests.remove(requestKey);
+
+                        DeviceStatus deviceStatus = new DeviceStatus(
+                            deviceAddr, buttonId, status, hexMessage);
+                        updateDeviceStatus(deviceCode, deviceStatus);
+
+                        log.info("[Keka86-Read] 更新设备:{}, 按键:{}, 状态:{}",
+                            deviceCode, buttonId, status == 1 ? "开" : "关");
+                        return; // 找到匹配的请求,处理完毕
+                    }
+                }
+            }
+
+            // 如果没有待处理请求匹配,可能是设备主动上报
+            log.info("[Keka86-Read] 网关:{} 收到状态上报(可能是主动上报), 状态:{}",
+                gatewayId, status == 1 ? "开" : "关");
+
+        }
+        catch (Exception e) {
+            log.error("[Keka86-Read] 网关:{}, 解析失败, Hex:{}", gatewayId, hexMessage, e);
+        }
+    }
+
+    /**
+     * 处理写入响应 (功能码 06H)
+     *
+     * 响应格式 (写入确认 - 回显请求):
+     * [0] 设备地址: 01
+     * [1] 功能码:   06
+     * [2] 寄存器高: 10
+     * [3] 寄存器低: 21 (0x1021 = 按键1)
+     * [4] 数据高:   00
+     * [5] 数据低:   01 或 00
+     * [6] CRC低
+     * [7] CRC高
+     *
+     * 示例: 01 06 10 21 00 01 DD 00 表示按键1设置为开
+     */
+    private void handleWriteResponse(String gatewayId, String hexMessage) {
+        try {
+            byte[] response = hexStringToBytes(hexMessage);
+
+            if (response.length < 8) {
+                log.warn("[Keka86-Write] 响应长度不足: {}", hexMessage);
+                return;
+            }
+
+            // CRC校验
+            byte[] dataWithoutCrc = new byte[6];
+            System.arraycopy(response, 0, dataWithoutCrc, 0, 6);
+            int calculatedCrc = calculateCRC16(dataWithoutCrc);
+            int receivedCrc = (response[6] & 0xFF) | ((response[7] & 0xFF) << 8);
+
+            if (calculatedCrc != receivedCrc) {
+                log.error("[Keka86-Write] CRC校验失败, 网关:{}, Hex:{}", gatewayId, hexMessage);
+                return;
+            }
+
+            // 解析寄存器地址和值
+            int registerAddr = ((response[2] & 0xFF) << 8) | (response[3] & 0xFF);
+            int dataValue = ((response[4] & 0xFF) << 8) | (response[5] & 0xFF);
+
+            // 根据寄存器地址推断按键ID
+            int buttonId = getButtonIdFromAddr(registerAddr);
+            int status = (dataValue == VALUE_ON) ? 1 : 0;
+
+            log.info("[Keka86-Write] 网关:{}, 写入确认, 寄存器:0x{}, 按键:{}, 状态:{}",
+                gatewayId, String.format("%04X", registerAddr),
+                buttonId, status == 1 ? "开" : "关");
+
+            // 更新数据库状态
+            String deviceCode = getDeviceCode(gatewayId, buttonId);
+            if (deviceCode != null) {
+                DeviceStatus deviceStatus = new DeviceStatus(
+                    response[0] & 0xFF, buttonId, status, hexMessage);
+                updateDeviceStatus(deviceCode, deviceStatus);
+            }
+            else {
+                log.warn("[Keka86-Write] 网关:{}, 按键{} 未找到对应设备", gatewayId, buttonId);
+            }
+
+        }
+        catch (Exception e) {
+            log.error("[Keka86-Write] 网关:{}, 解析失败, Hex:{}", gatewayId, hexMessage, e);
+        }
+    }
+
+    /**
+     * 处理错误响应
+     * 响应格式: 01 83 01 CRC_L CRC_H (读取错误)
+     * 01 86 01 CRC_L CRC_H (写入错误)
+     */
+    private void handleErrorResponse(String gatewayId, String hexMessage, byte originalFuncCode) {
+        try {
+            byte[] response = hexStringToBytes(hexMessage);
+            if (response.length >= 3) {
+                byte errorCode = response[2];
+                String errorMsg = getModbusErrorMessage(errorCode);
+
+                String funcName = (originalFuncCode == 0x03) ? "读取" : (originalFuncCode == 0x06) ? "写入" : "未知";
+
+                log.error("[Keka86-Error] 网关:{}, {}操作失败, 错误:{}, Hex:{}", gatewayId, funcName, errorMsg,
+                    hexMessage);
+            }
+        }
+        catch (Exception e) {
+            log.error("[Keka86-Error] 网关:{}, 错误响应解析失败, Hex:{}", gatewayId, hexMessage, e);
+        }
+    }
+
+    /**
+     * 更新设备状态到数据库
+     */
+    private void updateDeviceStatus(String deviceCode, DeviceStatus deviceStatus) {
+        try {
+            EmsObjAttrValue value = objAttrValueService.selectObjAttrValue(MODE_CODE, deviceCode, "Switch");
+            String newValue = String.valueOf(deviceStatus.getStatus());
+
+            if (null != value && !StringUtils.equals(value.getAttrValue(), newValue)) {
+                objAttrValueService.updateObjAttrValue(MODE_CODE, deviceCode, "Switch", newValue);
+                new EmsApiTemplate(emsConfig).notifyAttrValueChangedAsync(deviceCode, "Switch", value.getAttrValue(),
+                    newValue);
+                log.info("[Keka86-Update] 设备:{}, 状态更新: {} -> {}", deviceCode, value.getAttrValue(), newValue);
+            }
+            else if (null == value) {
+                value = new EmsObjAttrValue(deviceCode, MODE_CODE, "Switch", newValue);
+                objAttrValueService.mergeObjAttrValue(value);
+                log.info("[Keka86-Update] 设备:{}, 状态新增: {}", deviceCode, newValue);
+            }
+        }
+        catch (Exception e) {
+            log.error("[Keka86-Update] 设备:{}, 状态更新失败", deviceCode, e);
+        }
+    }
+}

+ 59 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/keka/model/DeviceStatus.java

@@ -0,0 +1,59 @@
+/*
+ * 文 件 名:  DeviceStatus
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/4
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.keka.model;
+
+/**
+ * 设备状态类
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/4]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+public class DeviceStatus {
+    private int deviceAddr;
+
+    private int buttonId;     // 灯ID (1/2/3)
+
+    private int status;      // 1=开, 0=关
+
+    private String rawResponse;
+
+    public DeviceStatus(int deviceAddr, int buttonId, int status, String rawResponse) {
+        this.deviceAddr = deviceAddr;
+        this.buttonId = buttonId;
+        this.status = status;
+        this.rawResponse = rawResponse;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("设备地址: %d, 灯%d状态: %s, 原始响应: %s", deviceAddr, buttonId, status == 1 ? "开" : "关",
+            rawResponse);
+    }
+
+    public int getDeviceAddr() {
+        return deviceAddr;
+    }
+
+    public int getButtonId() {
+        return buttonId;
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    public String getRawResponse() {
+        return rawResponse;
+    }
+}

+ 38 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/keka/model/ModbusCommand.java

@@ -0,0 +1,38 @@
+/*
+ * 文 件 名:  ModbusCommand
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/12/5
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.keka.model;
+
+/**
+ * <一句话功能简述>
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/12/5]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+public class ModbusCommand {
+    private final byte[] commandBytes;
+    private final String commandHex;
+
+    public ModbusCommand(byte[] commandBytes, String commandHex) {
+        this.commandBytes = commandBytes;
+        this.commandHex = commandHex;
+    }
+
+    public byte[] getCommandBytes() {
+        return commandBytes;
+    }
+
+    public String getCommandHex() {
+        return commandHex;
+    }
+}

+ 54 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/config/SquareLightCtlConfig.java

@@ -0,0 +1,54 @@
+/*
+ * 文 件 名:  SquareLightCtlConfig
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 广场灯控对接配置
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "adapter.square-lighting-ctl")
+public class SquareLightCtlConfig {
+    /**
+     * 平台地址
+     */
+    private String url;
+
+    /**
+     * 客户端id
+     */
+    private String clientId;
+
+    /**
+     * 用户名
+     */
+    private String userName;
+
+    /**
+     * 用户名密码
+     */
+    private String password;
+
+    /**
+     * 租户ID
+     */
+    private String tenantId;
+}

+ 78 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/controller/SquareLightCtlController.java

@@ -0,0 +1,78 @@
+/*
+ * 文 件 名:  SquareLightCtlController
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/19
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.controller;
+
+import com.ruoyi.ems.ba.controller.BaCtlController;
+import com.ruoyi.ems.squarelgt.handle.SquareLightCtlHandler;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.model.CallResponse;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * 广场灯控控制接口
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/19]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@RestController
+@CrossOrigin(allowedHeaders = "*", allowCredentials = "false")
+@RequestMapping("/square-light-ctl")
+@Api(value = "SquareLightCtlController", description = "广场灯控Api")
+public class SquareLightCtlController {
+    /**
+     * 日志
+     */
+    private static final Logger log = LoggerFactory.getLogger(BaCtlController.class);
+
+    @Qualifier("squareLightCtlHandler")
+    @Resource
+    private SquareLightCtlHandler squareLightCtlHandler;
+
+    /**
+     * 常泰室内能耗能力调用
+     *
+     * @return 数据列表
+     */
+    @RequestMapping(value = "/ct/abilityCall", method = RequestMethod.POST)
+    @ApiOperation(value = "/ct/abilityCall", notes = "照明系统能力调用")
+    @ApiResponses({ @ApiResponse(code = 200, message = "success"),
+        @ApiResponse(code = 400, message = "{code:****,message:'fail'}")
+    })
+    public CallResponse<Void> ctSlAbilityCall(@RequestBody AbilityPayload abilityPayload) {
+        CallResponse<Void> res = null;
+
+        try {
+            res = squareLightCtlHandler.call(abilityPayload);
+        }
+        catch (Exception e) {
+            log.error("ctSlAbilityCall fail!", e);
+            res = new CallResponse<>(501, "内部错误:" + e.getMessage());
+        }
+
+        return res;
+    }
+}

+ 735 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/handle/SquareLightCtlHandler.java

@@ -0,0 +1,735 @@
+/*
+ * 文 件 名:  SquareLightCtlHandler
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/19
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.handle;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.huashe.common.domain.JsonEntity;
+import com.huashe.common.exception.Assert;
+import com.ruoyi.ems.common.handle.BaseDevHandler;
+import com.ruoyi.ems.squarelgt.config.SquareLightCtlConfig;
+import com.ruoyi.ems.squarelgt.retrofit.SquareLightCtlTemplate;
+import com.ruoyi.ems.domain.EmsDevice;
+import com.ruoyi.ems.domain.EmsObjAttrValue;
+import com.ruoyi.ems.enums.DevObjType;
+import com.ruoyi.ems.model.AbilityPayload;
+import com.ruoyi.ems.common.model.CallData;
+import com.ruoyi.ems.model.CallResponse;
+import com.ruoyi.ems.model.QueryDevice;
+import com.ruoyi.ems.squarelgt.model.BaselineInfo;
+import com.ruoyi.ems.squarelgt.model.CcElecInfo;
+import com.ruoyi.ems.squarelgt.model.DevSub;
+import com.ruoyi.ems.squarelgt.model.DeviceInfo;
+import com.ruoyi.ems.squarelgt.model.SlcDevice;
+import com.ruoyi.ems.squarelgt.model.SlcDeviceLamp;
+import com.ruoyi.ems.squarelgt.model.SlcProject;
+import com.ruoyi.ems.squarelgt.model.SlcProjectSubset;
+import com.ruoyi.ems.squarelgt.model.StatusInfo;
+import com.ruoyi.ems.util.PingUtils;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 广场灯控
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/19]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Service
+public class SquareLightCtlHandler extends BaseDevHandler {
+    private static final Logger log = LoggerFactory.getLogger(SquareLightCtlHandler.class);
+
+    // 对接系统
+    private static final String SYS_MODE_CODE = "M_Z010_SYS_SQUARE_LIGHT";
+
+    // 灯杆模型代码
+    private static final String LAMP_POST_MODE_CODE = "M_Z010_DEV_SQUARE_LAMP_POST";
+
+    // 照明灯组模型代码
+    private static final String DEV_MODE_CODE = "M_Z010_DEV_SQUARE_LIGHT";
+
+    // 集中器设备代码
+    private static final String CONCENTRATOR = "M_Z010_DEV_SQUARE_CONCENTRATOR";
+
+    // 设备子系统代码
+    private static final String SUBSYSTEM_CODE = "SYS_ZHZM";
+
+    @Autowired
+    private SquareLightCtlConfig config;
+
+    @Override
+    public List<EmsDevice> getDeviceList() {
+        QueryDevice queryDevice = new QueryDevice();
+        queryDevice.setDeviceModel(DEV_MODE_CODE);
+        return deviceService.selectList(queryDevice);
+    }
+
+    @Override
+    public void refreshOnline() {
+        try {
+            // 1. 从URL中提取IP地址
+            String url = config.getUrl();
+            if (StringUtils.isBlank(url)) {
+                log.warn("广场灯控系统URL配置为空,跳过心跳检测");
+                return;
+            }
+
+            String ipAddress = PingUtils.extractIpFromUrl(url);
+            if (StringUtils.isBlank(ipAddress)) {
+                log.warn("无法从URL中提取IP地址: {}", url);
+                return;
+            }
+
+            // 2. 执行ping检测
+            boolean isOnline = PingUtils.pingHost(ipAddress, 3000); // 3秒超时
+
+            // 3. 获取当前系统状态
+            String statusKey = "interfaceStatus";
+
+            // 查询当前状态
+            EmsObjAttrValue currentStatus = objAttrValueService.selectObjAttrValue(SYS_MODE_CODE, SUBSYSTEM_CODE,
+                statusKey);
+
+            String newStatus = isOnline ? "1" : "0";
+            String oldStatus = currentStatus != null ? currentStatus.getAttrValue() : null;
+
+            // 4. 状态发生变化时更新并记录
+            if (!StringUtils.equals(oldStatus, newStatus)) {
+                log.info("能耗监测系统连接状态变化: {} -> {}, IP: {}", oldStatus, newStatus, ipAddress);
+
+                // 更新系统属性状态
+                objAttrValueService.updateObjAttrValue(SYS_MODE_CODE, SUBSYSTEM_CODE, statusKey, newStatus);
+
+                String key = isOnline ? "connect" : "disconnect";
+                triggerEvent(SYS_MODE_CODE, SUBSYSTEM_CODE, key, null, new Date());
+            }
+
+            // 5. 记录心跳日志(可选,用于调试)
+            log.debug("广场灯控系统心跳检测完成 - IP: {}, 状态: {}", ipAddress, isOnline ? "在线" : "离线");
+        }
+        catch (Exception e) {
+            log.error("广场灯控系统心跳检测异常", e);
+        }
+    }
+
+    @Override
+    public CallResponse<Void> call(AbilityPayload abilityParam) {
+        CallResponse<Void> callResponse = null;
+
+        try {
+            if (DevObjType.SYSTEM.getCode() == abilityParam.getObjType()) {
+                switch (abilityParam.getAbilityKey()) {
+                    case "GetProjectList":
+                        execGetProjectList();
+                        break;
+                    case "GetProjectSubsetList":
+                        execGetProjectSubsetList();
+                        break;
+                    case "GetDeviceList":
+                        CompletableFuture.runAsync(this::execGetDeviceList);
+                        break;
+                    case "SyncDevAttrAll":
+                        CompletableFuture.runAsync(this::execSyncDevAttrAll);
+                        break;
+                    case "lightControlAll":
+                        lightControlAll(abilityParam.getAbilityParam());
+                        break;
+                    default:
+                        throw new UnsupportedOperationException("不支持的abilityKey");
+                }
+            }
+            else if (DevObjType.DEVC.getCode() == abilityParam.getObjType() && StringUtils.equals(LAMP_POST_MODE_CODE,
+                abilityParam.getModelCode())) {
+                switch (abilityParam.getAbilityKey()) {
+                    case "lightControlAll":
+                        lampPostLightControl(abilityParam.getObjCode(), abilityParam.getAbilityParam());
+                        break;
+                    default:
+                        throw new UnsupportedOperationException("不支持的abilityKey");
+                }
+            }
+            else if (DevObjType.DEVC.getCode() == abilityParam.getObjType() && StringUtils.equals(DEV_MODE_CODE,
+                abilityParam.getModelCode())) {
+                switch (abilityParam.getAbilityKey()) {
+                    case "lightControl":
+                        lightControl(abilityParam.getObjCode(), abilityParam.getAbilityParam());
+                        break;
+                    case "SyncDevAttr":
+                        execSyncDevAttr(abilityParam.getObjCode());
+                        break;
+                    case "lightControlBrightness":
+                        lightControlBrightness(abilityParam.getObjCode(), abilityParam.getAbilityParam());
+                        break;
+                    default:
+                        throw new UnsupportedOperationException("不支持的abilityKey");
+                }
+            }
+
+            callResponse = new CallResponse<>(0, "成功");
+        }
+        catch (Exception e) {
+            callResponse = new CallResponse<>(-1, e.getMessage());
+            log.error("调用ability异常", e);
+        }
+
+        return callResponse;
+    }
+
+    public void execSyncDevAttrAll() {
+        SquareLightCtlTemplate template = new SquareLightCtlTemplate(config.getUrl());
+        CallData<SlcProject> projecCallData = template.getProjectList();
+        List<SlcProject> projects = projecCallData.getList();
+
+        if (CollectionUtils.isNotEmpty(projects)) {
+            for (SlcProject project : projects) {
+                CallData<SlcDevice> callData = template.getDeviceList(project.getProjectId(), null);
+
+                // 灯组设备同步
+                List<SlcDevice> lightDevs = callData.getList().stream()
+                    .filter(dev -> StringUtils.equals(dev.getDeviceModelId(), "1945454135655632897"))
+                    .collect(Collectors.toList());
+                mergeLightDevAttrs(project.getProjectId(), lightDevs);
+
+                // 集中器设备同步
+                List<SlcDevice> centDevs = callData.getList().stream()
+                    .filter(dev -> StringUtils.equals(dev.getDeviceModelId(), "1943504597877362689"))
+                    .collect(Collectors.toList());
+                mergeCentDevAttrValue(project.getProjectId(), centDevs);
+            }
+        }
+    }
+
+    public void execSyncDevAttr(String objCode) {
+        EmsObjAttrValue proAttr = objAttrValueService.selectObjAttrValue(DEV_MODE_CODE, objCode, "projectId");
+        SquareLightCtlTemplate template = new SquareLightCtlTemplate(config.getUrl());
+        mergeLightDevAttr(template, proAttr.getAttrValue(), objCode, null);
+    }
+
+    public void execGetProjectList() {
+        SquareLightCtlTemplate template = new SquareLightCtlTemplate(config.getUrl());
+        CallData<SlcProject> callData = template.getProjectList();
+        List<SlcProject> projects = callData.getList();
+        saveCallLog(SUBSYSTEM_CODE, SYS_MODE_CODE, "GetProjectList", callData.getCallStatus(),
+            callData.getCallPayload(), callData.getResPayload());
+        objAttrValueService.updateObjAttrValue(SYS_MODE_CODE, SUBSYSTEM_CODE, "projectList",
+            JSON.toJSONString(projects));
+    }
+
+    public void execGetProjectSubsetList() {
+        SquareLightCtlTemplate template = new SquareLightCtlTemplate(config.getUrl());
+        CallData<SlcProject> projectCallData = template.getProjectList();
+        List<SlcProject> projects = projectCallData.getList();
+
+        if (CollectionUtils.isNotEmpty(projects)) {
+            List<SlcProjectSubset> subsets = new ArrayList<>();
+
+            for (SlcProject project : projects) {
+                CallData<SlcProjectSubset> callData = template.getProjectSubsetList(project.getProjectId());
+                List<SlcProjectSubset> slcProjectSubsets = callData.getList();
+                saveCallLog(SUBSYSTEM_CODE, SYS_MODE_CODE, "GetProjectSubsetList", callData.getCallStatus(),
+                    callData.getCallPayload(), callData.getResPayload());
+                subsets.addAll(slcProjectSubsets);
+            }
+
+            objAttrValueService.updateObjAttrValue(SYS_MODE_CODE, SUBSYSTEM_CODE, "projectSubsetList",
+                JSON.toJSONString(subsets));
+        }
+    }
+
+    public void lightControlAll(String paramValue) {
+        QueryDevice queryDevice = new QueryDevice();
+        queryDevice.setDeviceModel(LAMP_POST_MODE_CODE);
+        queryDevice.setSubsystemCode(SUBSYSTEM_CODE);
+        queryDevice.setDeviceStatus(1);
+        List<EmsDevice> devices = deviceService.selectList(queryDevice);
+
+        if (CollectionUtils.isNotEmpty(devices)) {
+            for (EmsDevice device : devices) {
+                lampPostLightControl(device.getDeviceCode(), paramValue);
+            }
+        }
+    }
+
+    public void lampPostLightControl(String lampPostId, String paramValue) {
+        EmsObjAttrValue lampPostSubDev = objAttrValueService.selectObjAttrValue(LAMP_POST_MODE_CODE, lampPostId,
+            "subDev");
+
+        if (null != lampPostSubDev) {
+            JSONArray lights = JSONArray.parseArray(lampPostSubDev.getAttrValue());
+
+            JsonEntity.ObjBuilder json = JsonEntity.objBuilder();
+
+            lights.forEach(item -> {
+                JSONObject jsonObject = (JSONObject) item;
+                json.addKeyArray("deviceIds", jsonObject.getString("deviceCode"));
+            });
+
+            JSONObject jsonObject = (JSONObject) lights.get(0);
+
+            EmsObjAttrValue projectIdAttr = objAttrValueService.selectObjAttrValue(DEV_MODE_CODE,
+                jsonObject.getString("deviceCode"), "projectId");
+
+            JsonEntity.ObjBuilder lampListObj = JsonEntity.objBuilder().putKv("lampNo", "1")
+                .putKv("lampOnOff", paramValue).putKv("brightness", 0);
+            json.addKeyArray("lampList", lampListObj.build().getJsonObj());
+
+            // 下发执行命令
+            lightControl(projectIdAttr.getAttrValue(), json.build().getJsonObj());
+        }
+    }
+
+    public void lightControl(String objCode, String paramValue) {
+        List<EmsObjAttrValue> attrValues = objAttrValueService.selectByObjCode(DEV_MODE_CODE, objCode);
+        Map<String, EmsObjAttrValue> attrValueMap = attrValues.stream()
+            .collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, Function.identity()));
+
+        EmsObjAttrValue projectIdAttr = attrValueMap.get("projectId");
+        Assert.notNull(projectIdAttr, -1, "lampList 属性未同步");
+        EmsObjAttrValue deviceIdAttr = attrValueMap.get("deviceId");
+        Assert.notNull(deviceIdAttr, -1, "deviceId 属性未同步");
+        EmsObjAttrValue lampListAttr = attrValueMap.get("lampList");
+        Assert.notNull(lampListAttr, -1, "lampList 属性未同步");
+
+        JsonEntity.ObjBuilder json = JsonEntity.objBuilder().addKeyArray("deviceIds", deviceIdAttr.getAttrValue());
+
+        List<SlcDeviceLamp> lampList = JSONArray.parseArray(lampListAttr.getAttrValue(), SlcDeviceLamp.class);
+        Assert.notEmpty(lampList, -1, "灯组列表为空");
+
+        for (SlcDeviceLamp lamp : lampList) {
+            JsonEntity.ObjBuilder lampListObj = JsonEntity.objBuilder().putKv("lampNo", lamp.getLampNo())
+                .putKv("lampOnOff", paramValue).putKv("brightness", 0);
+
+            json.addKeyArray("lampList", lampListObj.build().getJsonObj());
+        }
+
+        // 下发执行命令
+        lightControl(projectIdAttr.getAttrValue(), json.build().getJsonObj());
+    }
+
+    /**
+     * 灯组开关控制
+     */
+    public void lightControl(String projectId, JSONObject req) {
+        SquareLightCtlTemplate template = new SquareLightCtlTemplate(config.getUrl());
+        CallData<Void> callData = template.lightControl(projectId, req);
+        saveCallLog(SUBSYSTEM_CODE, SYS_MODE_CODE, "lightControl", callData.getCallStatus(), callData.getCallPayload(),
+            callData.getResPayload());
+    }
+
+    public void lightControlBrightness(String objCode, String paramValue) {
+        List<EmsObjAttrValue> attrValues = objAttrValueService.selectByObjCode(DEV_MODE_CODE, objCode);
+        Map<String, EmsObjAttrValue> attrValueMap = attrValues.stream()
+            .collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, v -> v));
+
+        EmsObjAttrValue projectIdAttr = attrValueMap.get("projectId");
+        Assert.notNull(projectIdAttr, -1, "projectId 属性未同步");
+        EmsObjAttrValue deviceIdAttr = attrValueMap.get("deviceId");
+        Assert.notNull(deviceIdAttr, -1, "deviceId 属性未同步");
+        EmsObjAttrValue lampListAttr = attrValueMap.get("lampList");
+        Assert.notNull(lampListAttr, -1, "lampList 属性未同步");
+
+        JsonEntity.ObjBuilder json = JsonEntity.objBuilder().addKeyArray("deviceIds", deviceIdAttr.getAttrValue());
+
+        List<SlcDeviceLamp> lampList = JSONArray.parseArray(lampListAttr.getAttrValue(), SlcDeviceLamp.class);
+        Assert.notEmpty(lampList, -1, "灯组列表为空");
+
+        for (SlcDeviceLamp lamp : lampList) {
+            JsonEntity.ObjBuilder lampListObj = JsonEntity.objBuilder().putKv("lampNo", lamp.getLampNo())
+                .putKv("brightness", paramValue);
+
+            json.addKeyArray("lampList", lampListObj.build().getJsonObj());
+        }
+
+        // 下发控制命令
+        lightControlBrightness(projectIdAttr.getAttrValue(), json.build().getJsonObj());
+    }
+
+    /**
+     * 灯组亮度控制
+     */
+    public void lightControlBrightness(String projectId, JSONObject req) {
+        SquareLightCtlTemplate template = new SquareLightCtlTemplate(config.getUrl());
+        CallData<Void> callData = template.controlBrightness(projectId, req);
+        saveCallLog(SUBSYSTEM_CODE, SYS_MODE_CODE, "lightControlBrightness", callData.getCallStatus(),
+            callData.getCallPayload(), callData.getResPayload());
+    }
+
+    public void execGetDeviceList() {
+        SquareLightCtlTemplate template = new SquareLightCtlTemplate(config.getUrl());
+        CallData<SlcProject> projecCallData = template.getProjectList();
+        List<SlcProject> projects = projecCallData.getList();
+
+        if (CollectionUtils.isNotEmpty(projects)) {
+            for (SlcProject project : projects) {
+                CallData<SlcDevice> callData = template.getDeviceList(project.getProjectId(), null);
+                saveCallLog(SUBSYSTEM_CODE, SYS_MODE_CODE, "GetDeviceList", callData.getCallStatus(),
+                    callData.getCallPayload(), callData.getResPayload());
+
+                // 灯组设备同步
+                List<SlcDevice> lightDevs = callData.getList().stream()
+                    .filter(dev -> StringUtils.equals(dev.getDeviceModelId(), "1945454135655632897"))
+                    .collect(Collectors.toList());
+                syncLightDeviceList(lightDevs);
+                mergeLightDevAttrs(project.getProjectId(), lightDevs);
+
+                // 集中器设备同步
+                List<SlcDevice> centDevs = callData.getList().stream()
+                    .filter(dev -> StringUtils.equals(dev.getDeviceModelId(), "1943504597877362689"))
+                    .collect(Collectors.toList());
+                syncCentDeviceList(centDevs);
+                mergeCentDevAttrValue(project.getProjectId(), centDevs);
+            }
+        }
+    }
+
+    public void syncLightDeviceList(List<SlcDevice> slcDevices) {
+        // 查询数据库记录
+        QueryDevice param = new QueryDevice();
+        param.setDeviceModel(LAMP_POST_MODE_CODE);
+        param.setSubsystemCode(SUBSYSTEM_CODE);
+        List<EmsDevice> dbDevs = deviceService.selectList(param);
+
+        if (CollectionUtils.isNotEmpty(dbDevs)) {
+            for (EmsDevice emsDevice : dbDevs) {
+                deviceService.deleteDevice(LAMP_POST_MODE_CODE, emsDevice.getDeviceCode());
+                objAttrValueService.deleteByObjCode(LAMP_POST_MODE_CODE, emsDevice.getDeviceCode());
+            }
+        }
+
+        // 待新增的设备
+        if (CollectionUtils.isNotEmpty(slcDevices)) {
+            Map<String, SlcDevice> slcDevMap = slcDevices.stream()
+                .collect(Collectors.toMap(SlcDevice::getDeviceId, Function.identity()));
+
+            Map<String, List<String>> matchMap = mergeLightDev(slcDevices);
+
+            List<EmsDevice> addDevs = new ArrayList<>();
+            List<EmsObjAttrValue> attrValues = new ArrayList<>();
+
+            for (Map.Entry<String, List<String>> entry : matchMap.entrySet()) {
+                // 灯杆ID
+                String deviceCode = entry.getKey();
+                // 原始设备ID
+                List<String> deviceIds = entry.getValue();
+
+                SlcDevice slcDeviceTmp = slcDevMap.get(deviceIds.get(0));
+                addDevs.add(buildLampPostDevice(deviceCode, slcDeviceTmp));
+                attrValues.add(buildLampPostLights(deviceCode, deviceIds));
+            }
+
+            addDevs.sort(Comparator.comparing(EmsDevice::getDeviceName));
+            deviceService.insertBatch(addDevs);
+            objAttrValueService.insertBatch(attrValues);
+        }
+    }
+
+    private Map<String, List<String>> mergeLightDev(List<SlcDevice> slcDevices) {
+        Map<String, List<String>> ret = new HashMap<>();
+
+        for (SlcDevice device : slcDevices) {
+            // 获取deviceName的前缀(如"B1")
+            String prefix = "SL-Z-" + StringUtils.substringBefore(device.getDeviceName(), "-");
+
+            // 如果map中不存在该前缀,初始化一个空列表
+            ret.computeIfAbsent(prefix, k -> new ArrayList<>());
+
+            // 将当前deviceName添加到对应前缀的列表中
+            ret.get(prefix).add(device.getDeviceId());
+        }
+
+        return ret;
+    }
+
+    public void syncCentDeviceList(List<SlcDevice> slcDevices) {
+        // 查询数据库记录
+        QueryDevice param = new QueryDevice();
+        param.setDeviceModel(CONCENTRATOR);
+        param.setSubsystemCode(SUBSYSTEM_CODE);
+        List<EmsDevice> dbDevs = deviceService.selectList(param);
+        Set<String> dbDevCodeSet = dbDevs.stream().map(EmsDevice::getDeviceCode).collect(Collectors.toSet());
+
+        Map<String, SlcDevice> slcDevMap = slcDevices.stream()
+            .collect(Collectors.toMap(SlcDevice::getDeviceCode, Function.identity()));
+        Set<String> slcDevCodeSet = slcDevices.stream().map(SlcDevice::getDeviceCode).collect(Collectors.toSet());
+
+        // 待删除的设备
+        Set<String> delCodes = new HashSet<>(dbDevCodeSet);
+        delCodes.removeAll(slcDevCodeSet);
+
+        if (CollectionUtils.isNotEmpty(delCodes)) {
+            for (String code : delCodes) {
+                deviceService.deleteDevice(CONCENTRATOR, code);
+            }
+        }
+
+        // 待新增的设备
+        Set<String> addCodes = new HashSet<>(slcDevCodeSet);
+        addCodes.removeAll(dbDevCodeSet);
+
+        if (CollectionUtils.isNotEmpty(addCodes)) {
+            List<EmsDevice> addDevs = new ArrayList<>();
+
+            for (String code : addCodes) {
+                SlcDevice slcDevice = slcDevMap.get(code);
+                addDevs.add(buildCentDevice(slcDevice));
+            }
+
+            addDevs.sort(Comparator.comparing(EmsDevice::getDeviceName));
+            deviceService.insertBatch(addDevs);
+        }
+    }
+
+    private EmsDevice buildLampPostDevice(String deviceCode, SlcDevice slcDevice) {
+        EmsDevice device = new EmsDevice();
+        device.setDeviceCode(deviceCode);
+        device.setDeviceName(StringUtils.substringBefore(slcDevice.getDeviceName(), "-") + "灯杆");
+        device.setDeviceSpec("智慧灯杆");
+        device.setDeviceStatus(slcDevice.getStatus());
+        device.setDeviceModel(LAMP_POST_MODE_CODE);
+        device.setSubsystemCode(SUBSYSTEM_CODE);
+
+        if (StringUtils.startsWith(slcDevice.getDeviceName(), "B")) {
+            device.setLocation("北区广场");
+            device.setLocationRef("321283124S300104");
+            device.setAreaCode("321283124S3001");
+            device.setRefFacs("Z-ZM-01");
+        }
+        else if (StringUtils.startsWith(slcDevice.getDeviceName(), "N")) {
+            device.setLocation("南区广场");
+            device.setLocationRef("321283124S300204");
+            device.setAreaCode("321283124S3002");
+            device.setRefFacs("Z-ZM-02");
+        }
+
+        return device;
+    }
+
+    private EmsObjAttrValue buildLampPostLights(String deviceCode, List<String> lampList) {
+        JSONArray jsonArray = new JSONArray();
+
+        for (String lamp : lampList) {
+            JSONObject jsonObject = new JSONObject();
+            jsonObject.put("deviceCode", lamp);
+            jsonObject.put("modelCode", DEV_MODE_CODE);
+            jsonArray.add(jsonObject);
+        }
+
+        EmsObjAttrValue attrValue = new EmsObjAttrValue();
+        attrValue.setObjCode(deviceCode);
+        attrValue.setModelCode(LAMP_POST_MODE_CODE);
+        attrValue.setAttrKey("subDev");
+        attrValue.setAttrValue(jsonArray.toString());
+        return attrValue;
+    }
+
+    private EmsDevice buildCentDevice(SlcDevice slcDevice) {
+        EmsDevice device = new EmsDevice();
+        device.setDeviceCode(slcDevice.getDeviceCode());
+        device.setDeviceName(slcDevice.getDeviceName());
+        device.setDeviceBrand("佐通");
+        device.setDeviceSpec("单灯控制器");
+        device.setDeviceStatus(slcDevice.getStatus());
+        device.setDeviceModel(CONCENTRATOR);
+        device.setSubsystemCode(SUBSYSTEM_CODE);
+
+        if (StringUtils.startsWith(slcDevice.getDeviceName(), "北区")) {
+            device.setLocation("北区广场");
+            device.setLocationRef("321283124S300104");
+            device.setAreaCode("321283124S3001");
+            device.setRefFacs("DEV01");
+        }
+        else if (StringUtils.startsWith(slcDevice.getDeviceName(), "南区")) {
+            device.setLocation("南区广场");
+            device.setLocationRef("321283124S300204");
+            device.setAreaCode("321283124S3002");
+            device.setRefFacs("DEV02");
+        }
+
+        return device;
+    }
+
+    private void mergeLightDevAttrs(String projectId, List<SlcDevice> slcDevices) {
+        SquareLightCtlTemplate template = new SquareLightCtlTemplate(config.getUrl());
+
+        for (SlcDevice slcDevice : slcDevices) {
+            mergeLightDevAttr(template, projectId, slcDevice.getDeviceId(), slcDevice);
+        }
+    }
+
+    private void mergeLightDevAttr(SquareLightCtlTemplate template, String projectId, String deviceId,
+        SlcDevice slcDevice) {
+        CallData<DeviceInfo> callData = template.getDeviceInfo(projectId, deviceId);
+        DeviceInfo deviceInfo = callData.getData();
+
+        List<EmsObjAttrValue> oaValues = objAttrValueService.selectByObjCode(DEV_MODE_CODE, deviceId);
+
+        if (CollectionUtils.isNotEmpty(oaValues)) {
+            Map<String, EmsObjAttrValue> vMap = oaValues.stream()
+                .collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, Function.identity()));
+            checkAndUpdate(vMap, deviceId, CONCENTRATOR, "deviceStatus", convertDeviceStatus(slcDevice.getAbnormal()));
+
+            if (null != deviceInfo.getBaselineInfo()) {
+                BaselineInfo baselineInfo = deviceInfo.getBaselineInfo();
+                checkAndUpdate(vMap, deviceId, DEV_MODE_CODE, "firmwareVer", baselineInfo.getFirmwareVer());
+                checkAndUpdate(vMap, deviceId, DEV_MODE_CODE, "softwareVer", baselineInfo.getSoftwareVer());
+            }
+
+            if (null != deviceInfo.getStatusInfo()) {
+                StatusInfo sInfo = deviceInfo.getStatusInfo();
+                checkAndUpdate(vMap, deviceId, DEV_MODE_CODE, "csq", String.valueOf(sInfo.getCsq()));
+                checkAndUpdate(vMap, deviceId, DEV_MODE_CODE, "current", String.valueOf(sInfo.getCurrent()));
+                checkAndUpdate(vMap, deviceId, DEV_MODE_CODE, "power", String.valueOf(sInfo.getPower()));
+                checkAndUpdate(vMap, deviceId, DEV_MODE_CODE, "factor", String.valueOf(sInfo.getFactor()));
+                checkAndUpdate(vMap, deviceId, DEV_MODE_CODE, "voltage", String.valueOf(sInfo.getVoltage()));
+                checkAndUpdate(vMap, deviceId, DEV_MODE_CODE, "energy", String.valueOf(sInfo.getEnergy()));
+                checkAndUpdate(vMap, deviceId, DEV_MODE_CODE, "temperature", String.valueOf(sInfo.getTemperature()));
+            }
+
+            if (CollectionUtils.isNotEmpty(deviceInfo.getLampList())) {
+                List<SlcDeviceLamp> lampList = deviceInfo.getLampList();
+                SlcDeviceLamp lamp = lampList.get(0);
+                checkAndUpdate(vMap, deviceId, DEV_MODE_CODE, "cct", String.valueOf(lamp.getCct()));
+                checkAndUpdate(vMap, deviceId, DEV_MODE_CODE, "Switch", String.valueOf(lamp.getLampOnOff()));
+            }
+        }
+        else {
+            oaValues = new ArrayList<>();
+
+            oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "projectId", projectId));
+            oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "deviceId", deviceId));
+
+            if (null != slcDevice) {
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "deviceName", slcDevice.getDeviceName()));
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "imageUrl", slcDevice.getImageUrl()));
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "deviceStatus", convertDeviceStatus(slcDevice.getAbnormal())));
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "csq", slcDevice.getCsq()));
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "lampList", JSON.toJSONString(slcDevice.getLampList())));
+            }
+
+            if (null != deviceInfo.getDeviceInfo()) {
+                DevSub subInfo = deviceInfo.getDeviceInfo();
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "deviceUid", subInfo.getDeviceUid()));
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "minCct", String.valueOf(subInfo.getMinCct())));
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "maxCct", String.valueOf(subInfo.getMaxCct())));
+            }
+
+            if (null != deviceInfo.getBaselineInfo()) {
+                BaselineInfo bInfo = deviceInfo.getBaselineInfo();
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "deviceModelId", bInfo.getDeviceModelId()));
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "firmwareVer", bInfo.getFirmwareVer()));
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "softwareVer", bInfo.getSoftwareVer()));
+            }
+
+            if (null != deviceInfo.getStatusInfo()) {
+                StatusInfo sInfo = deviceInfo.getStatusInfo();
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "current", String.valueOf(sInfo.getCurrent())));
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "power", String.valueOf(sInfo.getPower())));
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "factor", String.valueOf(sInfo.getFactor())));
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "voltage", String.valueOf(sInfo.getVoltage())));
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "energy", String.valueOf(sInfo.getEnergy())));
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "frequency", String.valueOf(sInfo.getFrequency())));
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "temperature", String.valueOf(sInfo.getTemperature())));
+            }
+
+            if (CollectionUtils.isNotEmpty(deviceInfo.getLampList())) {
+                List<SlcDeviceLamp> lampList = deviceInfo.getLampList();
+                SlcDeviceLamp lamp = lampList.get(0);
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "cct", String.valueOf(lamp.getCct())));
+                oaValues.add(new EmsObjAttrValue(deviceId, DEV_MODE_CODE, "Switch", String.valueOf(lamp.getLampOnOff())));
+            }
+
+            objAttrValueService.insertBatch(oaValues);
+        }
+    }
+
+    private void mergeCentDevAttrValue(String projectId, List<SlcDevice> slcDevices) {
+        for (SlcDevice slc : slcDevices) {
+            List<EmsObjAttrValue> objAttrValues = objAttrValueService.selectByObjCode(CONCENTRATOR,
+                slc.getDeviceCode());
+            CcElecInfo info = slc.getCcElecInfo();
+
+            if (CollectionUtils.isNotEmpty(objAttrValues)) {
+                Map<String, EmsObjAttrValue> vMap = objAttrValues.stream().collect(Collectors.toMap(EmsObjAttrValue::getAttrKey, Function.identity()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "deviceStatus", convertDeviceStatus(slc.getAbnormal()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "csq", String.valueOf(slc.getCsq()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "kwh", String.valueOf(info.getKwh()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "kvarh", String.valueOf(info.getKvarh()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "ua", String.valueOf(info.getUa()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "ub", String.valueOf(info.getUb()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "uc", String.valueOf(info.getUc()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "ia", String.valueOf(info.getIa()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "ib", String.valueOf(info.getIb()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "ic", String.valueOf(info.getIc()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "pfa", String.valueOf(info.getPfa()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "pfb", String.valueOf(info.getPfb()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "pfc", String.valueOf(info.getPfc()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "hza", String.valueOf(info.getHza()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "hzb", String.valueOf(info.getHzb()));
+                checkAndUpdate(vMap, slc.getDeviceCode(), CONCENTRATOR, "hzc", String.valueOf(info.getHzc()));
+            }
+            else {
+                objAttrValues = new ArrayList<>();
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "projectId", projectId));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "deviceId", slc.getDeviceId()));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "deviceUid", slc.getDeviceUid()));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "deviceModelId", slc.getDeviceModelId()));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "imageUrl", slc.getImageUrl()));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "deviceStatus", convertDeviceStatus(slc.getAbnormal())));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "csq", String.valueOf(slc.getCsq())));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "kwh", String.valueOf(info.getKwh())));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "kvarh", String.valueOf(info.getKvarh())));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "ua", String.valueOf(info.getUa())));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "ub", String.valueOf(info.getUb())));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "uc", String.valueOf(info.getUc())));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "ia", String.valueOf(info.getIa())));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "ib", String.valueOf(info.getIb())));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "ic", String.valueOf(info.getIc())));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "pfa", String.valueOf(info.getPfa())));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "pfb", String.valueOf(info.getPfb())));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "pfc", String.valueOf(info.getPfc())));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "hza", String.valueOf(info.getHza())));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "hzb", String.valueOf(info.getHzb())));
+                objAttrValues.add(new EmsObjAttrValue(slc.getDeviceCode(), CONCENTRATOR, "hzc", String.valueOf(info.getHzc())));
+
+                objAttrValueService.insertBatch(objAttrValues);
+            }
+        }
+    }
+
+    private String convertDeviceStatus(Integer abnormal) {
+        if (null == abnormal) {
+            return "0";
+        }
+        else {
+            return abnormal == 0 ? "1" : "0";
+        }
+    }
+}

+ 40 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/AppLoginReq.java

@@ -0,0 +1,40 @@
+/*
+ * 文 件 名:  AppLoginReq
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.model;
+
+import lombok.Data;
+
+/**
+ * 登录请求
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class AppLoginReq {
+    // 登录类型3:账号密码
+    private int loginType;
+
+    // 客户端id
+    private String clientId;
+
+    // 用户名
+    private String username;
+
+    // 用户密码
+    private String password;
+
+    // 租户固定1
+    private String tenantId;
+}

+ 46 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/AppLoginRes.java

@@ -0,0 +1,46 @@
+/*
+ * 文 件 名:  AppLoginRes
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.model;
+
+import com.alibaba.fastjson2.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * 登录响应
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class AppLoginRes {
+    public String scope;
+
+    @JSONField(name = "openid")
+    public String openId;
+
+    @JSONField(name = "access_token")
+    public String accessToken;
+
+    @JSONField(name = "refresh_token")
+    public String refreshToken;
+
+    @JSONField(name = "expire_in")
+    public Long expireIn;
+
+    @JSONField(name = "refresh_expire_in")
+    public String refreshExpireIn;
+
+    @JSONField(name = "client_id")
+    public String clientId;
+}

+ 32 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/BaselineInfo.java

@@ -0,0 +1,32 @@
+/*
+ * 文 件 名:  BaselineInfo
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/29
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.model;
+
+import lombok.Data;
+
+/**
+ * BaselineInfo
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/29]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class BaselineInfo {
+    private String firmwareVer;
+    private String address;
+    private String createTime;
+    private String deviceModelName;
+    private String deviceModelId;
+    private String softwareVer;
+}

+ 40 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/CcElecInfo.java

@@ -0,0 +1,40 @@
+/*
+ * 文 件 名:  CcElecInfo
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/25
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.model;
+
+import lombok.Data;
+
+/**
+ * 电能参数
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/25]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class CcElecInfo {
+    private Double kwh;
+    private Double kvarh;
+    private Double ua;
+    private Double ub;
+    private Double uc;
+    private Double ia;
+    private Double ib;
+    private Double ic;
+    private Double pfa;
+    private Double pfb;
+    private Double pfc;
+    private Double hza;
+    private Double hzb;
+    private Double hzc;
+}

+ 36 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/DevSub.java

@@ -0,0 +1,36 @@
+/*
+ * 文 件 名:  DevInfo
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/10/10
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.model;
+
+import lombok.Data;
+
+/**
+ * <一句话功能简述>
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/10/10]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class DevSub {
+    private String lastReportTime;
+    private String deviceModelName;
+    private Integer maxCct;
+    private Integer minCct;
+    private String deviceUid;
+    private String deviceName;
+    private String deviceId;
+    private String subnetName;
+    private Integer deviceModelCctType;
+    private Integer status;
+}

+ 32 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/DeviceInfo.java

@@ -0,0 +1,32 @@
+/*
+ * 文 件 名:  DeviceInfoRes
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/29
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * DeviceInfoRes
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/29]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class DeviceInfo {
+    private StatusInfo statusInfo;
+    private BaselineInfo baselineInfo;
+    private DevSub deviceInfo;
+    private List<SlcDeviceLamp> lampList;
+}

+ 45 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/LightControlReq.java

@@ -0,0 +1,45 @@
+/*
+ * 文 件 名:  LightControlReq
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/19
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 灯控请求
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/19]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class LightControlReq {
+    /**
+     * 设备ID列表
+     */
+    private List<String> deviceIds;
+
+    /**
+     * 灯列表
+     */
+    private List<SlcDeviceLamp> lampList;
+
+    public LightControlReq() {
+    }
+
+    public LightControlReq(List<String> deviceIds, List<SlcDeviceLamp> lampList) {
+        this.deviceIds = deviceIds;
+        this.lampList = lampList;
+    }
+}

+ 55 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/SlcDevice.java

@@ -0,0 +1,55 @@
+/*
+ * 文 件 名:  SlcDevice
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 广场灯控设备
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class SlcDevice {
+    // 设备id设备名称
+    private String deviceName;
+    // 设备id
+    private String deviceId;
+    // 设备sn号
+    private String deviceUid;
+    // 设备状态 0离线1在线
+    private Integer status;
+    // 信号强度
+    private String csq;
+    // 0正常,1异常
+    private Integer abnormal;
+    // 图片
+    private String imageUrl;
+    // 数据更新时间
+    private String lastReportTime;
+    // 设备型号id
+    private String deviceModelId;
+    private Integer deviceCode;
+    private Integer minCct;
+    private Integer maxCct;
+    private CcElecInfo ccElecInfo;
+    private List<SlcDeviceLamp> lampList;
+
+    public String getDeviceCode() {
+        return "SL-Z-" + this.deviceUid;
+    }
+}

+ 88 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/SlcDeviceLamp.java

@@ -0,0 +1,88 @@
+/*
+ * 文 件 名:  SlcDeviceLamp
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.model;
+
+import lombok.Data;
+
+/**
+ * 设备灯组
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class SlcDeviceLamp {
+    // 灯具编号
+    private String lampNo;
+
+    // 灯开关状态 0关1开2关灯中3开灯中
+    private Integer lampOnOff;
+
+    // 功率w
+    private String power;
+
+    // 色温
+    private Integer cct;
+
+    // 亮度百分比
+    private Integer lampBright;
+
+    // 亮度值
+
+    private String brightness;
+
+    public SlcDeviceLamp() {
+    }
+
+    // 私有构造方法,只能通过Builder创建
+    private SlcDeviceLamp(Builder builder) {
+        this.lampNo = builder.lampNo;
+        this.lampOnOff = builder.lampOnOff;
+        this.brightness = builder.brightness;
+    }
+
+    // Builder静态内部类
+    public static class Builder {
+        private String lampNo;
+
+        private Integer lampOnOff;
+
+        private String brightness;
+
+        // 私有构造方法
+        private Builder() {
+        }
+
+        // 链式设值方法
+        public Builder lampNo(String lampNo) {
+            this.lampNo = lampNo;
+            return this;
+        }
+
+        public Builder lampOnOff(Integer lampOnOff) {
+            this.lampOnOff = lampOnOff;
+            return this;
+        }
+
+        public Builder brightness(String brightness) {
+            this.brightness = brightness;
+            return this;
+        }
+
+        // 构建实体对象
+        public SlcDeviceLamp build() {
+            return new SlcDeviceLamp(this);
+        }
+    }
+}

+ 42 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/SlcProject.java

@@ -0,0 +1,42 @@
+/*
+ * 文 件 名:  SlcProject
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.model;
+
+import lombok.Data;
+
+/**
+ * 灯控项目空间
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class SlcProject {
+    // 项目id
+    private String projectId;
+    // 项目名称
+    private String projectName;
+    // 项目地址
+    private String projectAddress;
+    // 项目子空间数量
+    private Integer projectSubsetNum;
+    // 项目设备数量
+    private Integer projectDeviceNum;
+    // 项目描述
+    private String remark;
+    // 创建时间
+    private String createTime;
+    // 项目类型
+    private String type;
+}

+ 29 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/SlcProjectSubset.java

@@ -0,0 +1,29 @@
+/*
+ * 文 件 名:  SlcProjectSubset
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.model;
+
+import lombok.Data;
+
+/**
+ * 项目分组
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class SlcProjectSubset {
+    private String subsetId;
+    private String subsetDeviceNum;
+    private String subsetName;
+}

+ 35 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/SlcResponse.java

@@ -0,0 +1,35 @@
+/*
+ * 文 件 名:  Response
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.model;
+
+import lombok.Data;
+
+/**
+ * 广场灯控平台响应
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class SlcResponse {
+    private int code;
+
+    private String msg;
+
+    private String data;
+
+    private Integer total;
+
+    private String rows;
+}

+ 37 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/StatusInfo.java

@@ -0,0 +1,37 @@
+/*
+ * 文 件 名:  StatusInfo
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/29
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.model;
+
+import lombok.Data;
+
+/**
+ * StatusInfo
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/29]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class StatusInfo {
+    private Integer csq;
+    private Double current;
+    private Long runingTime;
+    private Long runingTimeCurrent;
+    private String temperature;
+    private String angle;
+    private Double power;
+    private Double factor;
+    private Double voltage;
+    private Double energy;
+    private Integer frequency;
+}

+ 36 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/model/SyncStatusReq.java

@@ -0,0 +1,36 @@
+/*
+ * 文 件 名:  SyncStatusReq
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/19
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 同步设备状态请求
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/19]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Data
+public class SyncStatusReq {
+    private List<String> idList;
+
+    public SyncStatusReq() {
+    }
+
+    public SyncStatusReq(List<String> idList) {
+        this.idList = idList;
+    }
+}

+ 130 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/retrofit/SquareLightCtlApi.java

@@ -0,0 +1,130 @@
+/*
+ * 文 件 名:  SquareLightCtlApi
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.retrofit;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.ems.squarelgt.model.AppLoginReq;
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.GET;
+import retrofit2.http.Header;
+import retrofit2.http.Headers;
+import retrofit2.http.POST;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+/**
+ * 广场灯控接口
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+public interface SquareLightCtlApi {
+    /**
+     * 登陆
+     *
+     * @param req 请求body
+     * @return 登录响应
+     */
+    @Headers({ "tenantId: 1", "Content-Type: application/json" })
+    @POST("/light-cloud/app/auth/appLogin")
+    Call<String> appLogin(@Body AppLoginReq req);
+
+    /**
+     * 获取项目列表
+     *
+     * @param clientId      客户端id
+     * @param authorization 认证信息
+     * @return 项目列表
+     */
+    @Headers({ "tenantId: 1", "Content-Type: application/json" })
+    @GET("/light-cloud/app/project/list")
+    Call<String> getProjectList(@Header("clientId") String clientId, @Header("Authorization") String authorization);
+
+    /**
+     * 获取项目分组列表
+     *
+     * @param clientId      客户端id
+     * @param authorization 认证信息
+     * @param projectId     项目ID
+     * @return 项目列表
+     */
+    @Headers({ "tenantId: 1", "Content-Type: application/json" })
+    @GET("/light-cloud/app/projectSubset/list")
+    Call<String> getProjectSubsetList(@Header("clientId") String clientId,
+        @Header("Authorization") String authorization, @Query("projectId") String projectId);
+
+    /**
+     * 获取项目分组列表
+     *
+     * @param authorization 认证信息
+     * @param projectId     项目ID
+     * @param pageNum       页码
+     * @param pageSize      每页记录数
+     * @return 项目列表
+     */
+    @Headers({ "tenantId: 1", "Content-Type: application/json" })
+    @GET("/light-cloud/app/device/list")
+    Call<String> getDeviceList(@Header("clientId") String clientId, @Header("Authorization") String authorization,
+        @Header("projectId") String projectId, @Header("subsetId") String subsetId, @Query("pageNum") int pageNum,
+        @Query("pageSize") int pageSize);
+
+    /**
+     * 同步设备状态
+     *
+     * @param clientId 客户端id
+     * @param req      灯控请求
+     * @return 灯控响应
+     */
+    @Headers({ "tenantId: 1", "Content-Type: application/json" })
+    @POST("/light-cloud/operate/device/syncStatus")
+    Call<String> syncStatus(@Header("clientId") String clientId, @Header("projectId") String projectId,
+        @Header("Authorization") String authorization, @Body JSONObject req);
+
+    /**
+     * 获取设备状态
+     *
+     * @param clientId 客户端id
+     * @param deviceId 设备编号
+     * @return 灯控响应
+     */
+    @Headers({ "tenantId: 1", "Content-Type: application/json" })
+    @GET("/light-cloud/operate/device/light/info/{deviceId}")
+    Call<String> getDeviceInfo(@Header("clientId") String clientId, @Header("projectId") String projectId,
+        @Header("Authorization") String authorization, @Path("deviceId") String deviceId);
+
+    /**
+     * 设备开关灯批量-普通照明
+     *
+     * @param clientId 客户端id
+     * @param req      灯控请求
+     * @return 灯控响应
+     */
+    @Headers({ "tenantId: 1", "Content-Type: application/json" })
+    @POST("/light-cloud/app/device/light/control")
+    Call<String> lightControl(@Header("clientId") String clientId, @Header("projectId") String projectId,
+        @Header("Authorization") String authorization, @Body JSONObject req);
+
+    /**
+     * 设备调光批量-普通照明
+     *
+     * @param clientId 客户端id
+     * @param req      灯控请求
+     * @return 灯控响应
+     */
+    @Headers({ "tenantId: 1", "Content-Type: application/json" })
+    @POST("/light-cloud/operate/device/light/controlBrightness")
+    Call<String> controlBrightness(@Header("clientId") String clientId, @Header("projectId") String projectId,
+        @Header("Authorization") String authorization, @Body JSONObject req);
+}

+ 366 - 0
ems/ems-cloud/ems-dev-adapter/src/main/java/com/ruoyi/ems/squarelgt/retrofit/SquareLightCtlTemplate.java

@@ -0,0 +1,366 @@
+/*
+ * 文 件 名:  SquareLightCtlTemplate
+ * 版    权:  华设设计集团股份有限公司
+ * 描    述:  <描述>
+ * 修 改 人:  lvwenbin
+ * 修改时间:  2025/9/15
+ * 跟踪单号:  <跟踪单号>
+ * 修改单号:  <修改单号>
+ * 修改内容:  <修改内容>
+ */
+package com.ruoyi.ems.squarelgt.retrofit;
+
+import com.alibaba.fastjson.support.retrofit.Retrofit2ConverterFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.huashe.common.exception.Assert;
+import com.ruoyi.common.core.utils.SpringUtils;
+import com.ruoyi.ems.common.retrofit.BaseApiTemplate;
+import com.ruoyi.ems.squarelgt.config.SquareLightCtlConfig;
+import com.ruoyi.ems.common.model.CallData;
+import com.ruoyi.ems.squarelgt.model.AppLoginReq;
+import com.ruoyi.ems.squarelgt.model.AppLoginRes;
+import com.ruoyi.ems.squarelgt.model.DeviceInfo;
+import com.ruoyi.ems.squarelgt.model.SlcDevice;
+import com.ruoyi.ems.squarelgt.model.SlcProject;
+import com.ruoyi.ems.squarelgt.model.SlcProjectSubset;
+import com.ruoyi.ems.squarelgt.model.SlcResponse;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.OkHttpClient;
+import retrofit2.Call;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 广场灯控接口模板
+ * <功能详细描述>
+ *
+ * @author lvwenbin
+ * @version [版本号, 2025/9/15]
+ * @see [相关类/方法]
+ * @since [产品/模块版本]
+ */
+@Slf4j
+public class SquareLightCtlTemplate extends BaseApiTemplate {
+    /**
+     * 访问地址
+     */
+    protected String url;
+
+    /**
+     * 调用代理
+     */
+    protected final SquareLightCtlApi api;
+
+    public SquareLightCtlTemplate(String restUrl) {
+        this.url = restUrl;
+
+        OkHttpClient httpClient = getClient();
+        Retrofit retrofit = new Retrofit.Builder().baseUrl(restUrl)
+            .addConverterFactory(Retrofit2ConverterFactory.create()).client(httpClient).build();
+        this.api = retrofit.create(SquareLightCtlApi.class);
+    }
+
+    /**
+     * 构造调用模板
+     *
+     * @param restUrl        服务地址 (http://ip:port)
+     * @param connectTimeout 连接超时
+     * @param readTimeout    读取超时
+     * @param writeTimeout   写超时
+     */
+    public SquareLightCtlTemplate(String restUrl, int connectTimeout, int readTimeout, int writeTimeout) {
+        super.connectTimeout = connectTimeout;
+        super.readTimeout = readTimeout;
+        super.writeTimeout = writeTimeout;
+        this.url = restUrl;
+
+        OkHttpClient httpClient = getClient();
+        Retrofit retrofit = new Retrofit.Builder().baseUrl(restUrl)
+            .addConverterFactory(Retrofit2ConverterFactory.create()).client(httpClient).build();
+        api = retrofit.create(SquareLightCtlApi.class);
+    }
+
+    /**
+     * 登录接口
+     *
+     * @param cfg 请求参数
+     * @return 登录响应
+     */
+    public AppLoginRes appLogin(SquareLightCtlConfig cfg) {
+        AppLoginRes appLoginRes = null;
+
+        try {
+            AppLoginReq req = new AppLoginReq();
+            req.setLoginType(3);
+            req.setClientId(cfg.getClientId());
+            req.setUsername(cfg.getUserName());
+            req.setPassword(cfg.getPassword());
+            req.setTenantId(cfg.getTenantId());
+
+            // 执行调用
+            Call<String> call = api.appLogin(req);
+            Response<String> response = call.execute();
+            String res = response.body();
+            log.debug("appLogin response:{}", res);
+            Assert.notEmpty(res, -1, "获取Token失败");
+            SlcResponse slcRes = JSON.parseObject(res, SlcResponse.class);
+            Assert.notNull(slcRes, -1, "解析登录响应失败.");
+            Assert.isTrue(200 == slcRes.getCode(), -1, "登录失败:" + slcRes.getMsg());
+            appLoginRes = JSON.parseObject(slcRes.getData(), AppLoginRes.class);
+        }
+        catch (Exception e) {
+            log.error("appLogin fail!", e);
+        }
+
+        return appLoginRes;
+    }
+
+    public synchronized String getToken() {
+        SquareLightCtlConfig config = SpringUtils.getBean(SquareLightCtlConfig.class);
+        AppLoginRes res = appLogin(config);
+        return res.accessToken;
+    }
+
+    /**
+     * 获取项目列表
+     *
+     * @return 登录响应
+     */
+    public CallData<SlcProject> getProjectList() {
+        CallData<SlcProject> ret = null;
+        String token = null;
+        String resBody = null;
+
+        try {
+            SquareLightCtlConfig config = SpringUtils.getBean(SquareLightCtlConfig.class);
+            token = getToken();
+
+            Assert.notEmpty(token, -1, "获取Token失败");
+            token = "Bearer " + token;
+
+            // 执行调用
+            Call<String> call = api.getProjectList(config.getClientId(), token);
+            Response<String> response = call.execute();
+            resBody = response.body();
+
+            log.debug("getProjectList response:{}", resBody);
+            Assert.notEmpty(resBody, -1, "获取项目列表失败");
+            SlcResponse slcRes = JSON.parseObject(resBody, SlcResponse.class);
+            Assert.notNull(slcRes, -1, "解析项目列表响应失败.");
+            Assert.isTrue(200 == slcRes.getCode(), -1, slcRes.getMsg());
+            ret = CallData.<SlcProject>builder().addCallParam("ClientId", config.getClientId())
+                .addCallParam("token", token).setCallStatus(0).setResPayload(resBody)
+                .setList(JSON.parseArray(slcRes.getData(), SlcProject.class)).build();
+        }
+        catch (Exception e) {
+            log.error("getProjectList fail!", e);
+            ret = CallData.<SlcProject>builder().addCallParam("token", token).setCallStatus(2).setResPayload(resBody)
+                .build();
+        }
+
+        return ret;
+    }
+
+    /**
+     * 获取项目分组列表
+     *
+     * @return 登录响应
+     */
+    public CallData<SlcProjectSubset> getProjectSubsetList(String projectId) {
+        CallData<SlcProjectSubset> ret = null;
+        String token = null;
+        String resBody = null;
+
+        try {
+            SquareLightCtlConfig config = SpringUtils.getBean(SquareLightCtlConfig.class);
+
+            token = getToken();
+            Assert.notEmpty(token, -1, "获取Token失败");
+            token = "Bearer " + token;
+
+            Call<String> call = api.getProjectSubsetList(config.getClientId(), token, projectId);
+            Response<String> response = call.execute();
+            resBody = response.body();
+            log.debug("getProjectSubsetList response:{}", resBody);
+            Assert.notEmpty(resBody, -1, "获取项目分组列表失败");
+            SlcResponse slcRes = JSON.parseObject(resBody, SlcResponse.class);
+            Assert.notNull(slcRes, -1, "解析项目分组列表响应失败.");
+            Assert.isTrue(200 == slcRes.getCode(), -1, slcRes.getMsg());
+
+            ret = CallData.<SlcProjectSubset>builder().addCallParam("ClientId", config.getClientId())
+                .addCallParam("token", token).addCallParam("projectId", projectId).setCallStatus(0)
+                .setResPayload(resBody).setList(JSON.parseArray(slcRes.getData(), SlcProjectSubset.class)).build();
+
+        }
+        catch (Exception e) {
+            log.error("getProjectSubsetList fail!", e);
+            ret = CallData.<SlcProjectSubset>builder().addCallParam("token", token).addCallParam("projectId", projectId)
+                .setCallStatus(2).setResPayload(resBody).build();
+        }
+
+        return ret;
+    }
+
+    /**
+     * 获取设备列表
+     *
+     * @return 登录响应
+     */
+    public CallData<SlcDevice> getDeviceList(String projectId, String subsetId) {
+        CallData.Builder<SlcDevice> builder = CallData.builder();
+
+        try {
+            SquareLightCtlConfig config = SpringUtils.getBean(SquareLightCtlConfig.class);
+
+            String token = getToken();
+            Assert.notEmpty(token, -1, "获取Token失败");
+            token = "Bearer " + token;
+
+            int pageNum = 1;
+            int pageSize = 50;
+            int total = 0;
+
+            List<SlcDevice> list = new ArrayList<>();
+            builder.addCallParam("projectId", projectId).addCallParam("token", token);
+
+            do {
+                // 执行调用
+                Call<String> call = api.getDeviceList(config.getClientId(), token, projectId, subsetId, pageNum,
+                    pageSize);
+
+                Response<String> response = call.execute();
+                String resBody = response.body();
+                log.debug("getDeviceList response:{}", resBody);
+                builder.appendResPayload(String.format("response(pageNum-%d): %s", pageNum, System.lineSeparator()))
+                    .appendResPayload(resBody).appendResPayload(System.lineSeparator());
+                Assert.notEmpty(resBody, -1, "获取设备列表失败");
+                SlcResponse slcRes = JSON.parseObject(resBody, SlcResponse.class);
+                Assert.notNull(slcRes, -1, "解析设备列表响应失败.");
+                Assert.isTrue(200 == slcRes.getCode(), -1, slcRes.getMsg());
+                total = slcRes.getTotal();
+                list.addAll(JSON.parseArray(slcRes.getRows(), SlcDevice.class));
+                pageNum++;
+            }
+            while (list.size() < total);
+
+            builder.setList(list).setCallStatus(0);
+        }
+        catch (Exception e) {
+            builder.setCallStatus(2);
+            log.error("getDeviceList fail!", e);
+        }
+
+        return builder.build();
+    }
+
+    /**
+     * 获取设备
+     *
+     * @return 登录响应
+     */
+    public CallData<DeviceInfo> getDeviceInfo(String projectId, String deviceId) {
+        CallData.Builder<DeviceInfo> builder = CallData.builder();
+
+        try {
+            SquareLightCtlConfig config = SpringUtils.getBean(SquareLightCtlConfig.class);
+
+            String token = getToken();
+            Assert.notEmpty(token, -1, "获取Token失败");
+            token = "Bearer " + token;
+
+            // 执行调用
+            builder.addCallParam("projectId", projectId);
+            Call<String> call = api.getDeviceInfo(config.getClientId(), projectId, token, deviceId);
+            Response<String> response = call.execute();
+            String resBody = response.body();
+            log.debug("getDeviceInfo response:{}", resBody);
+            builder.appendResPayload(String.format("getDeviceInfo response: %s%s", resBody, System.lineSeparator()));
+            SlcResponse slcRes = JSON.parseObject(resBody, SlcResponse.class);
+            Assert.notNull(slcRes, -1, "解析设备状态响应失败.");
+            Assert.isTrue(200 == slcRes.getCode(), -1, slcRes.getMsg());
+            String data = slcRes.getData();
+            DeviceInfo deviceInfo = JSON.parseObject(data, DeviceInfo.class);
+            builder.setData(deviceInfo).setCallStatus(0);
+        }
+        catch (Exception e) {
+            builder.setCallStatus(2);
+            log.error("getDeviceInfo fail!", e);
+        }
+
+        return builder.build();
+    }
+
+    /**
+     * 照明控制
+     *
+     * @return 登录响应
+     */
+    public CallData<Void> lightControl(String projectId, JSONObject req) {
+        CallData.Builder<Void> builder = CallData.builder();
+
+        try {
+            SquareLightCtlConfig config = SpringUtils.getBean(SquareLightCtlConfig.class);
+
+            String token = getToken();
+            Assert.notEmpty(token, -1, "获取Token失败");
+            token = "Bearer " + token;
+
+            // 执行调用
+            builder.addCallParam("token", token).addCallParam("body", req.toString())
+                .addCallParam("projectId", projectId);
+            Call<String> call = api.lightControl(config.getClientId(), projectId, token, req);
+            Response<String> response = call.execute();
+            String resBody = response.body();
+            log.debug("lightControl response:{}", resBody);
+            builder.setResPayload(resBody).setCallStatus(0);
+            Assert.notEmpty(resBody, -1, "获取灯控响应失败");
+            SlcResponse slcRes = JSON.parseObject(resBody, SlcResponse.class);
+            Assert.notNull(slcRes, -1, "解析灯控响应失败.");
+            Assert.isTrue(200 == slcRes.getCode(), -1, slcRes.getMsg());
+        }
+        catch (Exception e) {
+            builder.setCallStatus(2);
+            log.error("lightControl fail!", e);
+        }
+
+        return builder.build();
+    }
+
+    /**
+     * 亮度调节
+     *
+     * @return 登录响应
+     */
+    public CallData<Void> controlBrightness(String projectId, JSONObject req) {
+        CallData.Builder<Void> builder = CallData.builder();
+
+        try {
+            SquareLightCtlConfig config = SpringUtils.getBean(SquareLightCtlConfig.class);
+
+            String token = getToken();
+            Assert.notEmpty(token, -1, "获取Token失败");
+            token = "Bearer " + token;
+
+            // 执行调用
+            builder.addCallParam("token", token).addCallParam("body", req.toString());
+            Call<String> call = api.controlBrightness(config.getClientId(), projectId, token, req);
+            Response<String> response = call.execute();
+            String resBody = response.body();
+            log.debug("controlBrightness response:{}", resBody);
+            builder.setResPayload(resBody).setCallStatus(0);
+            Assert.notEmpty(resBody, -1, "获取调光响应失败");
+            SlcResponse slcRes = JSON.parseObject(resBody, SlcResponse.class);
+            Assert.notNull(slcRes, -1, "解析调光响应失败.");
+            Assert.isTrue(200 == slcRes.getCode(), -1, slcRes.getMsg());
+        }
+        catch (Exception e) {
+            log.error("controlBrightness fail!", e);
+        }
+
+        return builder.build();
+    }
+}

+ 134 - 51
ems/ems-cloud/ems-dev-adapter/src/main/resources/logback.xml

@@ -1,74 +1,157 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <configuration scan="true" scanPeriod="60 seconds" debug="false">
     <!-- 日志存放路径 -->
-	<property name="log.path" value="logs/ems-dev-adapter" />
-   <!-- 日志输出格式 -->
-	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+    <property name="log.path" value="logs/ems-dev-adapter" />
+    <!-- 日志输出格式 -->
+    <property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
 
-    <!-- 控制台输出 -->
-	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
-		<encoder>
-			<pattern>${log.pattern}</pattern>
-		</encoder>
-	</appender>
-
-    <!-- 系统日志输出 -->
-	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
-	    <file>${log.path}/info.log</file>
-        <!-- 循环政策:基于时间创建日志文件 -->
-		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 日志文件名格式 -->
-			<fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
-			<!-- 日志最大的历史 60天 -->
-			<maxHistory>60</maxHistory>
-		</rollingPolicy>
-		<encoder>
-			<pattern>${log.pattern}</pattern>
-		</encoder>
-		<filter class="ch.qos.logback.classic.filter.LevelFilter">
-            <!-- 过滤的级别 -->
-            <level>INFO</level>
-            <!-- 匹配时的操作:接收(记录) -->
-            <onMatch>ACCEPT</onMatch>
-            <!-- 不匹配时的操作:拒绝(不记录) -->
-            <onMismatch>DENY</onMismatch>
-        </filter>
-	</appender>
+    <!-- 控制台输出(docker logs 看到的就是这个) -->
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
 
+    <!-- 全局错误日志 -->
     <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
-	    <file>${log.path}/error.log</file>
-        <!-- 循环政策:基于时间创建日志文件 -->
+        <file>${log.path}/error.log</file>
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 日志文件名格式 -->
             <fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
-			<!-- 日志最大的历史 60天 -->
-			<maxHistory>60</maxHistory>
+            <maxHistory>60</maxHistory>
         </rollingPolicy>
         <encoder>
             <pattern>${log.pattern}</pattern>
         </encoder>
         <filter class="ch.qos.logback.classic.filter.LevelFilter">
-            <!-- 过滤的级别 -->
             <level>ERROR</level>
-			<!-- 匹配时的操作:接收(记录) -->
             <onMatch>ACCEPT</onMatch>
-			<!-- 不匹配时的操作:拒绝(不记录) -->
             <onMismatch>DENY</onMismatch>
         </filter>
     </appender>
 
-    <!-- 系统模块日志级别控制  -->
-	<logger name="com.ruoyi" level="info" />
-	<!-- Spring日志级别控制  -->
-	<logger name="org.springframework" level="warn" />
+    <!-- ===================== 各模块独立日志 ===================== -->
+    <!-- 公共模块 -->
+    <appender name="common_appender" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/common.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/common.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder><pattern>${log.pattern}</pattern></encoder>
+    </appender>
+    <!-- 安科瑞 -->
+    <appender name="acrel_appender" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/acrel.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/acrel.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder><pattern>${log.pattern}</pattern></encoder>
+    </appender>
+    <!-- BA楼控 -->
+    <appender name="ba_appender" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/ba.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/ba.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder><pattern>${log.pattern}</pattern></encoder>
+    </appender>
+    <!-- 充电桩 -->
+    <appender name="charging_appender" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/charging.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/charging.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder><pattern>${log.pattern}</pattern></encoder>
+    </appender>
+    <!-- GeekOpen断路器 -->
+    <appender name="geekopen_appender" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/geekopen.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/geekopen.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder><pattern>${log.pattern}</pattern></encoder>
+    </appender>
+    <!-- 古瑞瓦特光伏 -->
+    <appender name="growatt_appender" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/growatt.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/growatt.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder><pattern>${log.pattern}</pattern></encoder>
+    </appender>
+    <!-- keka开关 -->
+    <appender name="keka_appender" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/keka.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/keka.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder><pattern>${log.pattern}</pattern></encoder>
+    </appender>
+    <!-- 广场灯控 -->
+    <appender name="squarelight_appender" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/squarelight.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/squarelight.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder><pattern>${log.pattern}</pattern></encoder>
+    </appender>
+
+    <!-- ===================== 包绑定 + 控制台输出全开 ===================== -->
+    <logger name="com.ruoyi.ems.acrel" level="info" additivity="false">
+        <appender-ref ref="acrel_appender" />
+        <appender-ref ref="console" />
+        <appender-ref ref="file_error" />
+    </logger>
+    <logger name="com.ruoyi.ems.ba" level="info" additivity="false">
+        <appender-ref ref="ba_appender" />
+        <appender-ref ref="console" />
+        <appender-ref ref="file_error" />
+    </logger>
+    <logger name="com.ruoyi.ems.charging" level="info" additivity="false">
+        <appender-ref ref="charging_appender" />
+        <appender-ref ref="console" />
+        <appender-ref ref="file_error" />
+    </logger>
+    <logger name="com.ruoyi.ems.common" level="info" additivity="false">
+        <appender-ref ref="common_appender" />
+        <appender-ref ref="console" />
+        <appender-ref ref="file_error" />
+    </logger>
+    <logger name="com.ruoyi.ems.geekopen" level="info" additivity="false">
+        <appender-ref ref="geekopen_appender" />
+        <appender-ref ref="console" />
+        <appender-ref ref="file_error" />
+    </logger>
+    <logger name="com.ruoyi.ems.growatt" level="info" additivity="false">
+        <appender-ref ref="growatt_appender" />
+        <appender-ref ref="console" />
+        <appender-ref ref="file_error" />
+    </logger>
+    <logger name="com.ruoyi.ems.keka" level="info" additivity="false">
+        <appender-ref ref="keka_appender" />
+        <appender-ref ref="console" />
+        <appender-ref ref="file_error" />
+    </logger>
+    <logger name="com.ruoyi.ems.squarelgt" level="info" additivity="false">
+        <appender-ref ref="squarelight_appender" />
+        <appender-ref ref="console" />
+        <appender-ref ref="file_error" />
+    </logger>
 
-	<root level="info">
-		<appender-ref ref="console" />
-	</root>
+    <!-- 基础包日志 -->
+    <logger name="com.ruoyi" level="info" />
+    <logger name="org.springframework" level="warn" />
 
-	<!--系统操作日志-->
-    <root level="INFO">
-        <appender-ref ref="file_info" />
+    <!-- root 只保留控制台 + 错误日志 -->
+    <root level="info">
+        <appender-ref ref="console" />
         <appender-ref ref="file_error" />
     </root>
-</configuration>
+</configuration>

+ 9 - 9
ems/ems-cloud/ems-dev-adapter/src/test/java/com/huashe/test/Acrel3000Test.java

@@ -13,15 +13,15 @@ package com.huashe.test;
 import com.huashe.common.utils.DateUtils;
 import com.ruoyi.common.redis.service.RedisService;
 import com.ruoyi.ems.EmsDevAdpApplication;
-import com.ruoyi.ems.config.AcrelConfig;
-import com.ruoyi.ems.core.Acrel3000Template;
-import com.ruoyi.ems.model.acrel.AlarmEventLog;
-import com.ruoyi.ems.model.acrel.Circuit;
-import com.ruoyi.ems.model.acrel.ConsumeReport;
-import com.ruoyi.ems.model.acrel.MeterKeyValue;
-import com.ruoyi.ems.model.acrel.MeterUse;
-import com.ruoyi.ems.model.acrel.Station;
-import com.ruoyi.ems.model.acrel.Token;
+import com.ruoyi.ems.acrel.config.AcrelConfig;
+import com.ruoyi.ems.acrel.retrofit.Acrel3000Template;
+import com.ruoyi.ems.acrel.model.AlarmEventLog;
+import com.ruoyi.ems.acrel.model.Circuit;
+import com.ruoyi.ems.acrel.model.ConsumeReport;
+import com.ruoyi.ems.acrel.model.MeterKeyValue;
+import com.ruoyi.ems.acrel.model.MeterUse;
+import com.ruoyi.ems.acrel.model.Station;
+import com.ruoyi.ems.acrel.model.Token;
 import lombok.extern.slf4j.Slf4j;
 import org.junit.Assert;
 import org.junit.Before;

+ 6 - 6
ems/ems-cloud/ems-dev-adapter/src/test/java/com/huashe/test/BaCtlTest.java

@@ -11,12 +11,12 @@
 package com.huashe.test;
 
 import com.ruoyi.ems.EmsDevAdpApplication;
-import com.ruoyi.ems.config.BaCtlConfig;
-import com.ruoyi.ems.core.BaCtlEnergyTemplate;
-import com.ruoyi.ems.model.CallData;
-import com.ruoyi.ems.model.idenergy.CodesVal;
-import com.ruoyi.ems.model.idenergy.CodesValReq;
-import com.ruoyi.ems.model.idenergy.CodesValSetReq;
+import com.ruoyi.ems.ba.config.BaCtlConfig;
+import com.ruoyi.ems.ba.retrofit.BaCtlEnergyTemplate;
+import com.ruoyi.ems.common.model.CallData;
+import com.ruoyi.ems.ba.model.CodesVal;
+import com.ruoyi.ems.ba.model.CodesValReq;
+import com.ruoyi.ems.ba.model.CodesValSetReq;
 import lombok.extern.slf4j.Slf4j;
 import org.junit.Assert;
 import org.junit.Test;

+ 4 - 5
ems/ems-cloud/ems-dev-adapter/src/test/java/com/huashe/test/GrowattTest.java

@@ -12,10 +12,10 @@ package com.huashe.test;
 
 import com.alibaba.fastjson2.JSONObject;
 import com.ruoyi.ems.EmsDevAdpApplication;
-import com.ruoyi.ems.config.GrowattConfig;
-import com.ruoyi.ems.core.GrowattTemplate;
-import com.ruoyi.ems.model.growatt.DataLogger;
-import com.ruoyi.ems.model.growatt.DeviceInfo;
+import com.ruoyi.ems.growatt.config.GrowattConfig;
+import com.ruoyi.ems.growatt.retrofit.GrowattTemplate;
+import com.ruoyi.ems.growatt.model.DataLogger;
+import com.ruoyi.ems.growatt.model.DeviceInfo;
 import lombok.extern.slf4j.Slf4j;
 import org.junit.Assert;
 import org.junit.Test;
@@ -26,7 +26,6 @@ import org.springframework.test.context.junit4.SpringRunner;
 import javax.annotation.Resource;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
 
 /**
  * GrowattTest

+ 9 - 9
ems/ems-cloud/ems-dev-adapter/src/test/java/com/huashe/test/SquareLightCtlTest.java

@@ -13,15 +13,15 @@ package com.huashe.test;
 import com.alibaba.fastjson2.JSONObject;
 import com.huashe.common.domain.JsonEntity;
 import com.ruoyi.ems.EmsDevAdpApplication;
-import com.ruoyi.ems.config.SquareLightCtlConfig;
-import com.ruoyi.ems.core.SquareLightCtlTemplate;
-import com.ruoyi.ems.handle.SquareLightCtlHandler;
-import com.ruoyi.ems.model.CallData;
-import com.ruoyi.ems.model.slightctl.AppLoginRes;
-import com.ruoyi.ems.model.slightctl.DeviceInfo;
-import com.ruoyi.ems.model.slightctl.SlcDevice;
-import com.ruoyi.ems.model.slightctl.SlcProject;
-import com.ruoyi.ems.model.slightctl.SlcProjectSubset;
+import com.ruoyi.ems.squarelgt.config.SquareLightCtlConfig;
+import com.ruoyi.ems.squarelgt.retrofit.SquareLightCtlTemplate;
+import com.ruoyi.ems.squarelgt.handle.SquareLightCtlHandler;
+import com.ruoyi.ems.common.model.CallData;
+import com.ruoyi.ems.squarelgt.model.AppLoginRes;
+import com.ruoyi.ems.squarelgt.model.DeviceInfo;
+import com.ruoyi.ems.squarelgt.model.SlcDevice;
+import com.ruoyi.ems.squarelgt.model.SlcProject;
+import com.ruoyi.ems.squarelgt.model.SlcProjectSubset;
 import lombok.extern.slf4j.Slf4j;
 import org.junit.Assert;
 import org.junit.Test;