package com.usky.vpp.util; import com.usky.vpp.domain.VppDevice; import com.usky.vpp.domain.VppResourcePoint; import com.usky.vpp.service.vo.CapabilityLoadPointVO; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; /** * 可调能力评估辅助(TSDB 聚合 + 模拟数据兜底) */ public final class VppCapabilityEvalHelper { public static final String CATEGORY_TOTAL = "TOTAL"; public static final String CATEGORY_LOAD = "LOAD"; public static final String CATEGORY_STORAGE = "STORAGE"; public static final String CATEGORY_GENERATION = "GENERATION"; private static final Set LOAD_TYPES = new HashSet<>(Arrays.asList("IND_LOAD", "COM_BLDG", "EVCS")); private static final Set STORAGE_TYPES = Collections.singleton("ESS"); private static final Set GENERATION_TYPES = Collections.singleton("PV"); private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("HH:mm"); private VppCapabilityEvalHelper() { } public static String categoryLabel(String category) { if (category == null) { return "未知"; } switch (category) { case CATEGORY_TOTAL: return "总资源"; case CATEGORY_LOAD: return "负荷类"; case CATEGORY_STORAGE: return "储能类"; case CATEGORY_GENERATION: return "发电类"; default: return category; } } public static String resolveCategory(String resourceType) { if (resourceType == null) { return CATEGORY_LOAD; } String type = resourceType.toUpperCase(); if (GENERATION_TYPES.contains(type)) { return CATEGORY_GENERATION; } if (STORAGE_TYPES.contains(type)) { return CATEGORY_STORAGE; } if (LOAD_TYPES.contains(type)) { return CATEGORY_LOAD; } return CATEGORY_LOAD; } public static String eventTypeLabel(Integer eventType) { if (eventType == null) { return "未知"; } return eventType == 1 ? "削峰" : eventType == 2 ? "填谷" : "未知"; } public static String responseTypeLabel(Integer responseType) { if (responseType == null) { return "未知"; } switch (responseType) { case 1: return "日前"; case 2: return "日内"; case 3: return "秒级"; default: return "未知"; } } /** * 基于 TDengine 历史数据构建负荷曲线(每 10 分钟一个点) *

实际负荷取统计日实测;基线负荷取 7 日前同时段作为参考基线。

*/ public static List buildLoadCurveFromTsdb( Map>> actualData, Map>> baselineData, LocalDate date, List powerMetrics) { List points = new ArrayList<>(); for (int minute = 0; minute < 1440; minute += 10) { LocalTime time = LocalTime.of(minute / 60, minute % 60); LocalDateTime actualStart = date.atTime(time); LocalDateTime actualEnd = actualStart.plusMinutes(10).minusSeconds(1); LocalDateTime baselineStart = date.minusDays(7).atTime(time); LocalDateTime baselineEnd = baselineStart.plusMinutes(10).minusSeconds(1); BigDecimal actual = sumBucketPower(actualData, powerMetrics, actualStart, actualEnd); BigDecimal baseline = sumBucketPower(baselineData, powerMetrics, baselineStart, baselineEnd); if (baseline == null || baseline.compareTo(BigDecimal.ZERO) <= 0) { baseline = actual; } CapabilityLoadPointVO point = new CapabilityLoadPointVO(); point.setTime(time.format(TIME_FMT)); point.setBaselineLoadKw(scale(baseline)); point.setActualLoadKw(scale(actual)); points.add(point); } return points; } /** * 汇总设备最新上下调能力(TDengine 实时指标) */ public static BigDecimal sumLatestMetric(Map> latestData, List metricCandidates) { if (latestData == null || latestData.isEmpty()) { return BigDecimal.ZERO; } BigDecimal total = BigDecimal.ZERO; for (Map metrics : latestData.values()) { BigDecimal value = pickMetric(metrics, metricCandidates); if (value != null) { total = total.add(value); } } return total.setScale(2, RoundingMode.HALF_UP); } public static boolean hasTsdbPowerData(Map>> deviceMetricData, List powerMetrics) { if (deviceMetricData == null || deviceMetricData.isEmpty()) { return false; } for (Map> metricMap : deviceMetricData.values()) { for (String metric : powerMetrics) { TreeMap series = metricMap.get(metric); if (series != null && !series.isEmpty()) { return true; } } } return false; } /** * 生成当日负荷曲线(每 10 分钟一个点,00:00 ~ 23:50)- 模拟兜底 */ public static List buildLoadCurve(List resources, List devices, Long siteId, LocalDate date) { List points = new ArrayList<>(); long seed = (siteId != null ? siteId : 0L) + date.toEpochDay(); for (int minute = 0; minute < 1440; minute += 10) { LocalTime time = LocalTime.of(minute / 60, minute % 60); double hourFactor = timeFactor(time); BigDecimal baseline = aggregateLoad(resources, devices, seed, minute, hourFactor, 0); BigDecimal actual = aggregateLoad(resources, devices, seed, minute, hourFactor, 1); CapabilityLoadPointVO point = new CapabilityLoadPointVO(); point.setTime(time.format(TIME_FMT)); point.setBaselineLoadKw(baseline); point.setActualLoadKw(actual); points.add(point); } return points; } private static BigDecimal aggregateLoad(List resources, List devices, long seed, int minuteOfDay, double hourFactor, int seriesSalt) { if (resources == null || resources.isEmpty()) { double base = 800 * hourFactor + pseudoRandom(seed, minuteOfDay, seriesSalt) * 120; return BigDecimal.valueOf(base).setScale(2, RoundingMode.HALF_UP); } BigDecimal total = BigDecimal.ZERO; for (VppResourcePoint resource : resources) { VppDevice device = findDevice(devices, resource.getDeviceId()); if (device != null && device.getCommStatus() != null && device.getCommStatus() == 0) { continue; } BigDecimal cap = resource.getCapacityKw() != null ? resource.getCapacityKw() : BigDecimal.valueOf(100); double typeFactor = typeLoadFactor(resource.getResourceType()); double noise = 0.85 + pseudoRandom(seed, resource.getId() != null ? resource.getId() : 0L, minuteOfDay, seriesSalt) * 0.3; double value = cap.doubleValue() * hourFactor * typeFactor * noise; if (CATEGORY_GENERATION.equals(resolveCategory(resource.getResourceType()))) { value *= 0.15; } total = total.add(BigDecimal.valueOf(value)); } if (seriesSalt == 1) { total = total.multiply(BigDecimal.valueOf(1.02 + pseudoRandom(seed, 999, minuteOfDay, seriesSalt) * 0.08)); } return total.setScale(2, RoundingMode.HALF_UP); } private static BigDecimal sumBucketPower(Map>> deviceData, List metricCandidates, LocalDateTime bucketStart, LocalDateTime bucketEnd) { if (deviceData == null || deviceData.isEmpty()) { return BigDecimal.ZERO; } BigDecimal total = BigDecimal.ZERO; for (Map> metricMap : deviceData.values()) { TreeMap series = pickSeries(metricMap, metricCandidates); if (series == null || series.isEmpty()) { continue; } BigDecimal avg = averageInRange(series, bucketStart, bucketEnd); if (avg != null) { total = total.add(avg); } } return total; } private static TreeMap pickSeries(Map> metricMap, List metricCandidates) { if (metricMap == null || metricMap.isEmpty()) { return null; } for (String metric : metricCandidates) { TreeMap series = metricMap.get(metric); if (series != null && !series.isEmpty()) { return series; } } for (Map.Entry> entry : metricMap.entrySet()) { if (entry.getValue() != null && !entry.getValue().isEmpty()) { return entry.getValue(); } } return null; } private static BigDecimal pickMetric(Map metrics, List metricCandidates) { if (metrics == null || metrics.isEmpty()) { return null; } for (String metric : metricCandidates) { BigDecimal value = metrics.get(metric); if (value != null) { return value; } } return null; } private static BigDecimal averageInRange(TreeMap series, LocalDateTime startInclusive, LocalDateTime endInclusive) { BigDecimal sum = BigDecimal.ZERO; int count = 0; for (Map.Entry entry : series.subMap(startInclusive, true, endInclusive, true).entrySet()) { if (entry.getValue() != null) { sum = sum.add(entry.getValue()); count++; } } if (count == 0) { Map.Entry floor = series.floorEntry(startInclusive); if (floor != null && floor.getValue() != null) { return floor.getValue(); } return null; } return sum.divide(BigDecimal.valueOf(count), 4, RoundingMode.HALF_UP); } private static BigDecimal scale(BigDecimal value) { if (value == null) { return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); } return value.setScale(2, RoundingMode.HALF_UP); } private static VppDevice findDevice(List devices, Long deviceId) { if (devices == null || deviceId == null) { return null; } return devices.stream().filter(d -> deviceId.equals(d.getId())).findFirst().orElse(null); } /** * 日内负荷形态:夜间低、早晚峰、午间次峰 */ private static double timeFactor(LocalTime time) { int hour = time.getHour(); int minute = time.getMinute(); double h = hour + minute / 60.0; if (h < 6) { return 0.45 + h / 6 * 0.15; } if (h < 9) { return 0.6 + (h - 6) / 3 * 0.35; } if (h < 12) { return 0.95 + (h - 9) / 3 * 0.05; } if (h < 14) { return 0.85; } if (h < 17) { return 0.9 + (h - 14) / 3 * 0.1; } if (h < 21) { return 1.0 - (h - 17) / 4 * 0.35; } return 0.5 - (h - 21) / 3 * 0.15; } private static double typeLoadFactor(String resourceType) { if (resourceType == null) { return 0.7; } switch (resourceType.toUpperCase()) { case "IND_LOAD": return 0.75; case "COM_BLDG": return 0.65; case "EVCS": return 0.55; case "ESS": return 0.4; case "PV": return 1.0; default: return 0.7; } } private static double pseudoRandom(long seed, long a, int b, int c) { long mixed = seed * 31L + a * 17L + b * 13L + c * 7L; return (mixed % 1000) / 1000.0; } private static double pseudoRandom(long seed, int minuteOfDay, int seriesSalt) { long mixed = seed * 31L + minuteOfDay * 17L + seriesSalt * 13L; return (mixed % 1000) / 1000.0; } }