|
@@ -0,0 +1,341 @@
|
|
|
+/*
|
|
|
+ * 文 件 名: ElecConsumeForecastService
|
|
|
+ * 版 权: 华设设计集团股份有限公司
|
|
|
+ * 描 述: <描述>
|
|
|
+ * 修 改 人: lvwenbin
|
|
|
+ * 修改时间: 2025/6/24
|
|
|
+ * 跟踪单号: <跟踪单号>
|
|
|
+ * 修改单号: <修改单号>
|
|
|
+ * 修改内容: <修改内容>
|
|
|
+ */
|
|
|
+package com.ruoyi.ems.service.analysis;
|
|
|
+
|
|
|
+import com.huashe.common.domain.model.WeatherForecast;
|
|
|
+import com.huashe.common.utils.DateUtils;
|
|
|
+import com.ruoyi.ems.domain.Area;
|
|
|
+import com.ruoyi.ems.domain.ElecConsumeForecast;
|
|
|
+import com.ruoyi.ems.domain.ElecMeterH;
|
|
|
+import com.ruoyi.ems.domain.EmsFacs;
|
|
|
+import com.ruoyi.ems.domain.MeterBoundaryRel;
|
|
|
+import com.ruoyi.ems.mapper.ElecConsumeForecastMapper;
|
|
|
+import com.ruoyi.ems.model.QueryMeter;
|
|
|
+import com.ruoyi.ems.service.IElecMeterHService;
|
|
|
+import com.ruoyi.ems.service.IMeterBoundaryRelService;
|
|
|
+import com.ruoyi.ems.service.IWeatherService;
|
|
|
+import org.apache.commons.collections4.CollectionUtils;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.ZoneId;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 用电消耗预测服务
|
|
|
+ * <功能详细描述>
|
|
|
+ *
|
|
|
+ * @author lvwenbin
|
|
|
+ * @version [版本号, 2025/6/24]
|
|
|
+ * @see [相关类/方法]
|
|
|
+ * @since [产品/模块版本]
|
|
|
+ */
|
|
|
+@Service
|
|
|
+public class ElecConsumeForecastService {
|
|
|
+ public enum FacilityType {
|
|
|
+ LIGHTING, // 照明设施
|
|
|
+ AIR_CONDITION, // 空调设施
|
|
|
+ PUBLIC, // 公共设施
|
|
|
+ OTHER // 其他设施
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(ElecConsumeForecastService.class);
|
|
|
+
|
|
|
+ private final DateTimeFormatter dateForm = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
+
|
|
|
+ // 温度对空调用电的影响因子表
|
|
|
+ private static final Map<Integer, Double> tempToAcFactor = new HashMap<>();
|
|
|
+
|
|
|
+ // 光照时间对照明用电的影响因子表
|
|
|
+ private static final Map<Integer, Double> lightHourToLightFactor = new HashMap<>();
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ElecConsumeForecastMapper forecastMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private IMeterBoundaryRelService meterBoundaryRelService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private IElecMeterHService elecMeterHService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private IWeatherService weatherService;
|
|
|
+
|
|
|
+ static {
|
|
|
+ // 初始化空调影响因子(温度-影响因子映射)
|
|
|
+ for (int i = 15; i <= 40; i++) {
|
|
|
+ if (i < 22) {
|
|
|
+ tempToAcFactor.put(i, 0.3 + (22 - i) * 0.05); // 15℃时0.65,22℃时0.3
|
|
|
+ }
|
|
|
+ else if (i <= 28) {
|
|
|
+ tempToAcFactor.put(i, 0.3 + (i - 22) * 0.1); // 22℃时0.3,28℃时0.9
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ tempToAcFactor.put(i, 0.9 + (i - 28) * 0.1); // 28℃时0.9,40℃时2.1
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化照明影响因子(光照时间-影响因子映射,假设夏季光照时间长)
|
|
|
+ for (int i = 8; i <= 16; i++) {
|
|
|
+ lightHourToLightFactor.put(i, 1.5 - (i - 8) * 0.1); // 8小时光照时1.5,16小时时0.7
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 地理区块用能预测
|
|
|
+ public void forecastAreas(String adcode, String rootArea, Area area, LocalDate today, int forecastDay) {
|
|
|
+ try {
|
|
|
+ // 获取区块绑定的计量设备
|
|
|
+ List<MeterBoundaryRel> boundaryRels = meterBoundaryRelService.selectRelByObj(area.getAreaCode(), 1, 45);
|
|
|
+
|
|
|
+ if (CollectionUtils.isEmpty(boundaryRels)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Set<String> meterDevs = getMeterDevCodes(boundaryRels);
|
|
|
+ // 获取历史数据 (过去30天)
|
|
|
+ List<ElecMeterH> historicalData = getMeterDayList(rootArea, meterDevs, today);
|
|
|
+
|
|
|
+ if (CollectionUtils.isEmpty(historicalData)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取天气预报
|
|
|
+ LocalDate forecastStart = today;
|
|
|
+ LocalDate forecastEnd = today.plusDays(forecastDay - 1);
|
|
|
+ List<WeatherForecast> weatherForecasts = weatherService.getWeatherForecast(adcode,
|
|
|
+ forecastStart.format(dateForm), forecastEnd.format(dateForm));
|
|
|
+ Map<String, WeatherForecast> weatherMap = convertToWeatherMap(weatherForecasts);
|
|
|
+
|
|
|
+ List<ElecConsumeForecast> results = new ArrayList<>();
|
|
|
+
|
|
|
+ while (forecastStart.isBefore(forecastEnd) || forecastStart.isEqual(forecastEnd)) {
|
|
|
+ WeatherForecast weatherForecast = weatherMap.get(forecastStart.format(dateForm));
|
|
|
+ double consumeForecast = calcAreaprediction(historicalData, weatherForecast);
|
|
|
+
|
|
|
+ ElecConsumeForecast forecast = new ElecConsumeForecast();
|
|
|
+ forecast.setAreaCode(rootArea);
|
|
|
+ forecast.setObjCode(area.getAreaCode());
|
|
|
+ forecast.setObjType(1);
|
|
|
+ forecast.setDate(Date.from(forecastStart.atStartOfDay(ZoneId.systemDefault()).toInstant()));
|
|
|
+ forecast.setElecUseQuantity(consumeForecast);
|
|
|
+
|
|
|
+ results.add(forecast);
|
|
|
+ forecastStart = forecastStart.plusDays(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ forecastMapper.deleteByCondition(rootArea, 1, area.getAreaCode(), today.format(dateForm),
|
|
|
+ forecastEnd.format(dateForm));
|
|
|
+ forecastMapper.insertBatch(results);
|
|
|
+ }
|
|
|
+ catch (Exception e) {
|
|
|
+ log.error("forecastAreas fail! areaCode:{}", area.getAreaCode(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设施用能预测
|
|
|
+ public void forecastFacilities(String adcode, String rootArea, EmsFacs facs, LocalDate today, int forecastDay) {
|
|
|
+ // 获取设施绑定的计量设备
|
|
|
+ List<MeterBoundaryRel> boundaryRels = meterBoundaryRelService.selectRelByObj(facs.getFacsCode(), 2, 45);
|
|
|
+
|
|
|
+ if (CollectionUtils.isEmpty(boundaryRels)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Set<String> meterDevs = getMeterDevCodes(boundaryRels);
|
|
|
+ // 获取历史数据 (过去30天)
|
|
|
+ List<ElecMeterH> historicalData = getMeterDayList(rootArea, meterDevs, today);
|
|
|
+
|
|
|
+ if (CollectionUtils.isEmpty(historicalData)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取天气预报
|
|
|
+ LocalDate forecastStart = today;
|
|
|
+ LocalDate forecastEnd = today.plusDays(forecastDay - 1);
|
|
|
+ List<WeatherForecast> weatherForecasts = weatherService.getWeatherForecast(adcode,
|
|
|
+ forecastStart.format(dateForm), forecastEnd.format(dateForm));
|
|
|
+ Map<String, WeatherForecast> weatherMap = convertToWeatherMap(weatherForecasts);
|
|
|
+
|
|
|
+ List<ElecConsumeForecast> results = new ArrayList<>();
|
|
|
+
|
|
|
+ while (forecastStart.isBefore(forecastEnd) || forecastStart.isEqual(forecastEnd)) {
|
|
|
+ WeatherForecast weatherForecast = weatherMap.get(forecastStart.format(dateForm));
|
|
|
+ double consumeForecast = calcFacsPrediction(facs, historicalData, weatherForecast);
|
|
|
+
|
|
|
+ ElecConsumeForecast forecast = new ElecConsumeForecast();
|
|
|
+ forecast.setAreaCode(rootArea);
|
|
|
+ forecast.setObjCode(facs.getFacsCode());
|
|
|
+ forecast.setObjType(2);
|
|
|
+ forecast.setDate(Date.from(forecastStart.atStartOfDay(ZoneId.systemDefault()).toInstant()));
|
|
|
+ forecast.setElecUseQuantity(consumeForecast);
|
|
|
+
|
|
|
+ results.add(forecast);
|
|
|
+ forecastStart = forecastStart.plusDays(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ forecastMapper.deleteByCondition(rootArea, 2, facs.getFacsCode(), today.format(dateForm),
|
|
|
+ forecastEnd.format(dateForm));
|
|
|
+ forecastMapper.insertBatch(results);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Set<String> getMeterDevCodes(List<MeterBoundaryRel> boundaryRels) {
|
|
|
+ Set<String> meterDevs = new HashSet<>();
|
|
|
+
|
|
|
+ for (MeterBoundaryRel meterBoundaryRel : boundaryRels) {
|
|
|
+ meterDevs.add(meterBoundaryRel.getMeterDevice());
|
|
|
+ }
|
|
|
+
|
|
|
+ return meterDevs;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, WeatherForecast> convertToWeatherMap(List<WeatherForecast> weatherForecasts) {
|
|
|
+ Map<String, WeatherForecast> weatherMap = new HashMap<>();
|
|
|
+
|
|
|
+ for (WeatherForecast weatherForecast : weatherForecasts) {
|
|
|
+ String date = DateUtils.dateToString(weatherForecast.getDate(), "yyyy-MM-dd");
|
|
|
+ weatherMap.put(date, weatherForecast);
|
|
|
+ }
|
|
|
+
|
|
|
+ return weatherMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ private double calcAreaprediction(List<ElecMeterH> historicalData, WeatherForecast weather) {
|
|
|
+ // 计算历史平均日用电量
|
|
|
+ double totalConsumeElec = historicalData.stream().mapToDouble(ElecMeterH::getElecQuantity).sum();
|
|
|
+ double historicalAvg = divideAndRound(totalConsumeElec, historicalData.size(), 2);
|
|
|
+
|
|
|
+ // 获取天气影响因子(区块主要受空调影响)
|
|
|
+ double weatherFactor = getAirConditionImpactFactor(weather);
|
|
|
+
|
|
|
+ // 计算预测值
|
|
|
+ return multiplyAndRound(historicalAvg, weatherFactor, 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ private double calcFacsPrediction(EmsFacs emsFacs, List<ElecMeterH> historicalData, WeatherForecast weather) {
|
|
|
+ FacilityType facilityType = determineFacilityType(emsFacs.getFacsSubCategory());
|
|
|
+
|
|
|
+ // 计算历史平均日用电量
|
|
|
+ double totalConsumeElec = historicalData.stream().mapToDouble(ElecMeterH::getElecQuantity).sum();
|
|
|
+ double historicalAvg = divideAndRound(totalConsumeElec, historicalData.size(), 2);
|
|
|
+
|
|
|
+ // 获取影响因子
|
|
|
+ double factor = getFacilityImpactFactor(weather, facilityType);
|
|
|
+
|
|
|
+ // 计算预测值
|
|
|
+ return multiplyAndRound(historicalAvg, factor, 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<ElecMeterH> getMeterDayList(String rootArea, Set<String> meterDevs, LocalDate startDate) {
|
|
|
+ LocalDate historyStart = startDate.minusDays(30);
|
|
|
+ LocalDate historyEnd = startDate.minusDays(1);
|
|
|
+
|
|
|
+ QueryMeter queryMeter = new QueryMeter();
|
|
|
+ queryMeter.setAreaCode(rootArea);
|
|
|
+ queryMeter.setDeviceCodes(meterDevs);
|
|
|
+ queryMeter.setStartRecTime(historyStart.format(dateForm));
|
|
|
+ queryMeter.setEndRecTime(historyEnd.format(dateForm));
|
|
|
+
|
|
|
+ return elecMeterHService.staMeterDayList(queryMeter);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 除法运算并保留指定小数位数
|
|
|
+ *
|
|
|
+ * @param dividend 被除数
|
|
|
+ * @param divisor 除数
|
|
|
+ * @param scale 保留小数位数
|
|
|
+ * @return 四舍五入后的结果
|
|
|
+ */
|
|
|
+ private double divideAndRound(double dividend, double divisor, int scale) {
|
|
|
+ if (divisor == 0) {
|
|
|
+ return 0.0;
|
|
|
+ }
|
|
|
+
|
|
|
+ return BigDecimal.valueOf(dividend / divisor).setScale(scale, RoundingMode.HALF_UP).doubleValue();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取空调设备的天气影响因子
|
|
|
+ public double getAirConditionImpactFactor(WeatherForecast weather) {
|
|
|
+ double temp = Math.max((weather.getDayTemp() != null) ? weather.getDayTemp() : 20,
|
|
|
+ (weather.getNightTemp() != null) ? weather.getNightTemp() : 20);
|
|
|
+ int tempInt = (int) Math.round(temp);
|
|
|
+ return tempToAcFactor.getOrDefault(tempInt, 1.0);
|
|
|
+ }
|
|
|
+
|
|
|
+ private double getFacilityImpactFactor(WeatherForecast weather, FacilityType type) {
|
|
|
+ switch (type) {
|
|
|
+ case AIR_CONDITION:
|
|
|
+ return getAirConditionImpactFactor(weather);
|
|
|
+ case LIGHTING:
|
|
|
+ return getLightingImpactFactor(weather);
|
|
|
+ default:
|
|
|
+ return 1.0; // 公共设施和其他设施默认影响因子为1
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private double getLightingImpactFactor(WeatherForecast weather) {
|
|
|
+ LocalDate date = weather.getDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
|
|
+ int month = date.getMonthValue();
|
|
|
+ int lightHours;
|
|
|
+
|
|
|
+ // 简化处理:根据月份估算光照时间(实际应结合天气预报的日照时长)
|
|
|
+ if (month >= 5 && month <= 8) {
|
|
|
+ lightHours = 14; // 夏季光照时间长
|
|
|
+ }
|
|
|
+ else if (month >= 12 || month <= 2) {
|
|
|
+ lightHours = 10; // 冬季光照时间短
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ lightHours = 12; // 春秋季
|
|
|
+ }
|
|
|
+
|
|
|
+ return lightHourToLightFactor.getOrDefault(lightHours, 1.0);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 乘法运算并保留指定小数位数
|
|
|
+ *
|
|
|
+ * @param a 乘数1
|
|
|
+ * @param b 乘数2
|
|
|
+ * @param scale 保留小数位数
|
|
|
+ * @return 四舍五入后的结果
|
|
|
+ */
|
|
|
+ private double multiplyAndRound(double a, double b, int scale) {
|
|
|
+ return BigDecimal.valueOf(a * b).setScale(scale, RoundingMode.HALF_UP).doubleValue();
|
|
|
+ }
|
|
|
+
|
|
|
+ private FacilityType determineFacilityType(String subcategory) {
|
|
|
+ if (subcategory.startsWith("Z010")) { // 照明
|
|
|
+ return FacilityType.LIGHTING;
|
|
|
+ }
|
|
|
+ else if (subcategory.startsWith("Z020")) { // 空调
|
|
|
+ return FacilityType.AIR_CONDITION;
|
|
|
+ }
|
|
|
+ else if (subcategory.startsWith("Z040")) { // 公共设施(充电桩等)
|
|
|
+ return FacilityType.PUBLIC;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ return FacilityType.OTHER;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|