|
|
@@ -10,15 +10,21 @@ import java.time.LocalDate;
|
|
|
import java.time.ZoneOffset;
|
|
|
import java.time.ZonedDateTime;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.Date;
|
|
|
import java.util.Locale;
|
|
|
import java.util.Random;
|
|
|
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.scheduling.annotation.Scheduled;
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
|
import com.ufo.base.framework.config.ProjectConfig;
|
|
|
+import com.ufo.project.model.domain.DataElectricityLocal;
|
|
|
+import com.ufo.project.model.mapper.DataElectricityLocalMapper;
|
|
|
+import com.ufo.project.weather.domain.DataWeatherLocal;
|
|
|
+import com.ufo.project.weather.mapper.DataWeatherLocalMapper;
|
|
|
|
|
|
/**
|
|
|
* 功率与气象mock数据生成任务
|
|
|
@@ -40,6 +46,10 @@ import com.ufo.base.framework.config.ProjectConfig;
|
|
|
* 文件名时间为UTC, date_time列为GMT+8(即文件名时间+8h);
|
|
|
* t2m/rh2/sp/云量/风/ghi/dni/dhi尽量符合物理规律, 按北纬38.9度定标, 同样按起报时间可复现.
|
|
|
*
|
|
|
+ * 另含采集数据mock任务(generateCollectedData): 每5分钟向 data_weather_local 与
|
|
|
+ * data_electricity_local 各插1行, tm取当前最近5分钟整北京时间, config_station_id固定为2,
|
|
|
+ * 两行共享同一时刻物理状态(云量/钟形/季节), 数据尽量接近真实, 按北京日期可复现.
|
|
|
+ *
|
|
|
* @author ufo
|
|
|
* @date 2026-10-21
|
|
|
*/
|
|
|
@@ -48,6 +58,12 @@ public class MockDataTask
|
|
|
{
|
|
|
private static final Logger log = LoggerFactory.getLogger(MockDataTask.class);
|
|
|
|
|
|
+ @Autowired
|
|
|
+ private DataWeatherLocalMapper dataWeatherLocalMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private DataElectricityLocalMapper dataElectricityLocalMapper;
|
|
|
+
|
|
|
/** UTC时区 */
|
|
|
private static final ZoneOffset UTC = ZoneOffset.UTC;
|
|
|
|
|
|
@@ -84,6 +100,15 @@ public class MockDataTask
|
|
|
/** 出力上限 */
|
|
|
private static final double POWER_CAP = 2600.0;
|
|
|
|
|
|
+ /** 采集数据mock固定场站ID(config_station_id) */
|
|
|
+ private static final long COLLECT_STATION_ID = 2L;
|
|
|
+
|
|
|
+ /** 采集数据mock装机容量(kW) */
|
|
|
+ private static final double COLLECT_CAPACITY_KW = 150000.0;
|
|
|
+
|
|
|
+ /** 采集数据mock理论出力定标的晴空峰值辐照度(W/m², 夏季正午晴空) */
|
|
|
+ private static final double CLEAR_PEAK_REF = 850.0;
|
|
|
+
|
|
|
/** 短期气象数据点数(自起报时间(含)起960个间隔, 961点) */
|
|
|
private static final int WEATHER_POINTS = 961;
|
|
|
|
|
|
@@ -210,6 +235,233 @@ public class MockDataTask
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 采集数据: 每5分钟执行一次, 向 data_weather_local 与 data_electricity_local 各插1行,
|
|
|
+ * tm为当前时间就近取整到最近5分钟整北京时间(如00:05:34→00:05:00, 00:09:12→00:10:00),
|
|
|
+ * config_station_id固定为2; 两行共享同一时刻的物理状态(云量/钟形/季节), 功率与气象描述同一天气;
|
|
|
+ * 理论出力为按150000kW装机容量定标的晴空钟形(夏峰~150000/冬峰~112500, 夜间0),
|
|
|
+ * 实发功率=理论×慢快云量衰减×可用率(恒≤理论), 随机参数按北京日期派生, 当日数据稳定可复现
|
|
|
+ */
|
|
|
+ @Scheduled(cron = "0 0/5 * * * ?")
|
|
|
+ public void generateCollectedData()
|
|
|
+ {
|
|
|
+ ZonedDateTime tm = roundToFiveMinute(ZonedDateTime.now(SHANGHAI));
|
|
|
+ try
|
|
|
+ {
|
|
|
+ DayPhysics phys = dayPhysics(tm.toLocalDate());
|
|
|
+ double tH = tm.toInstant().getEpochSecond() / 3600.0;
|
|
|
+ double cosz = coszAt(phys, tm);
|
|
|
+ double cloud = tccAt(phys, tH) / 100.0;
|
|
|
+ double peak = 750 + 100 * phys.season;
|
|
|
+ double ghi = 0.90 * peak * (1 - 0.95 * cloud) * cosz + 0.10 * peak * cosz * (1 + cloud);
|
|
|
+ double ghiClear = peak * cosz * (0.9 + 0.1 * cosz);
|
|
|
+
|
|
|
+ double hour = tm.getHour() + tm.getMinute() / 60.0;
|
|
|
+ double t2m = 17 + 7 * phys.season + 5 * Math.cos(2 * Math.PI * (hour - 14.5) / 24);
|
|
|
+ double rh2 = clamp(85 - 2.2 * t2m + 12 * cloud, 35, 98);
|
|
|
+ double spHpa = 1013 + 3 * Math.sin(2 * Math.PI * tH / phys.tP + phys.pP);
|
|
|
+ double spd10 = Math.max(0.3, phys.windBase + Math.sin(2 * Math.PI * tH / 24 + phys.pW));
|
|
|
+ double dir10 = wrapDir(phys.dirBase
|
|
|
+ + 40 * Math.sin(2 * Math.PI * tH / phys.tD + phys.pD)
|
|
|
+ + 12 * Math.sin(2 * Math.PI * tH / phys.tD2 + phys.pD2));
|
|
|
+
|
|
|
+ double theoretical = clamp(COLLECT_CAPACITY_KW * ghiClear / CLEAR_PEAK_REF, 0, COLLECT_CAPACITY_KW);
|
|
|
+ double fastFluct = 1 - phys.fastAmp * (0.5 + 0.5 * Math.sin(2 * Math.PI * tH / phys.fastT + phys.fastP));
|
|
|
+ double actual = clamp(theoretical * (1 - 0.90 * cloud) * fastFluct * phys.availability / 100.0, 0, theoretical);
|
|
|
+
|
|
|
+ Date tmDate = tm.toInstant().toDate();
|
|
|
+
|
|
|
+ DataWeatherLocal weather = new DataWeatherLocal();
|
|
|
+ weather.setTm(tmDate);
|
|
|
+ weather.setConfigStationId(COLLECT_STATION_ID);
|
|
|
+ weather.setIrradiance(round(ghi, 2));
|
|
|
+ weather.setInclinedIrradiance(round(ghi * (1 + 0.12 * cosz), 2));
|
|
|
+ weather.setTemperature(round(t2m, 2));
|
|
|
+ weather.setBoardTemperature(round(t2m + 0.031 * ghi, 2));
|
|
|
+ weather.setPressure(round(spHpa, 2));
|
|
|
+ weather.setHumidity(round(rh2, 1));
|
|
|
+ weather.setWindSpeed(round(spd10, 2));
|
|
|
+ weather.setWindDirection(round(dir10, 1));
|
|
|
+ weather.setDailyIrradiance(round(dailyIrradianceMj(phys, tm), 3));
|
|
|
+ dataWeatherLocalMapper.insertDataWeatherLocal(weather);
|
|
|
+
|
|
|
+ DataElectricityLocal power = new DataElectricityLocal();
|
|
|
+ power.setTm(tmDate);
|
|
|
+ power.setConfigStationId(COLLECT_STATION_ID);
|
|
|
+ power.setActualPower(round(actual, 1));
|
|
|
+ power.setTheoreticalPower(round(theoretical, 1));
|
|
|
+ power.setCurtailmentPower(0.0);
|
|
|
+ power.setAvailability(round(phys.availability, 2));
|
|
|
+ power.setDeviceStatus(0);
|
|
|
+ power.setGridStatus(1);
|
|
|
+ dataElectricityLocalMapper.insertDataElectricityLocal(power);
|
|
|
+
|
|
|
+ log.info("[MockDataTask] 生成采集数据mock, tm={}", tm.format(TIME_SH_FMT));
|
|
|
+ }
|
|
|
+ catch (Exception e)
|
|
|
+ {
|
|
|
+ log.error("[MockDataTask] 生成采集数据mock失败, tm={}", tm, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 当前北京时间就近取整到最近5分钟整点(如00:05:34→00:05:00, 00:09:12→00:10:00),
|
|
|
+ * 跨午夜时自动进位到次日00:00
|
|
|
+ */
|
|
|
+ private static ZonedDateTime roundToFiveMinute(ZonedDateTime now)
|
|
|
+ {
|
|
|
+ long secs = now.getHour() * 3600L + now.getMinute() * 60L + now.getSecond();
|
|
|
+ long rounded = ((secs + 150) / 300) * 300;
|
|
|
+ LocalDate day = now.toLocalDate();
|
|
|
+ if (rounded >= 86400L)
|
|
|
+ {
|
|
|
+ day = day.plusDays(1);
|
|
|
+ rounded -= 86400L;
|
|
|
+ }
|
|
|
+ return day.atStartOfDay(SHANGHAI).plusSeconds(rounded);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 采集数据mock: 由北京日期派生当日物理参数
|
|
|
+ * (种子为当日北京午夜的epoch分钟, 当日结果稳定可复现, 跨天换新云图/风场)
|
|
|
+ */
|
|
|
+ private static DayPhysics dayPhysics(LocalDate day)
|
|
|
+ {
|
|
|
+ DayPhysics p = new DayPhysics();
|
|
|
+ p.season = Math.cos(2 * Math.PI * (day.getDayOfYear() - 195.0) / 365.0);
|
|
|
+ double halfDay = 5.9 + 1.4 * p.season;
|
|
|
+ p.sunrise = 12 - halfDay;
|
|
|
+ p.sunset = 12 + halfDay;
|
|
|
+
|
|
|
+ Random random = new Random(day.atStartOfDay(SHANGHAI).toInstant().toEpochMilli() / 60000L);
|
|
|
+ p.baseCc = 20 + 40 * random.nextDouble();
|
|
|
+ p.ampCc = 10 + 20 * random.nextDouble();
|
|
|
+ p.tCc = 48 + 24 * random.nextDouble();
|
|
|
+ p.pCc = 2 * Math.PI * random.nextDouble();
|
|
|
+ p.tP = 96 + 48 * random.nextDouble();
|
|
|
+ p.pP = 2 * Math.PI * random.nextDouble();
|
|
|
+ p.windBase = 3 + 2 * random.nextDouble();
|
|
|
+ p.pW = 2 * Math.PI * random.nextDouble();
|
|
|
+ p.dirBase = 360 * random.nextDouble();
|
|
|
+ p.tD = 48 + 48 * random.nextDouble();
|
|
|
+ p.pD = 2 * Math.PI * random.nextDouble();
|
|
|
+ p.tD2 = 6 + 8 * random.nextDouble();
|
|
|
+ p.pD2 = 2 * Math.PI * random.nextDouble();
|
|
|
+ p.fastAmp = 0.02 + 0.08 * random.nextDouble();
|
|
|
+ p.fastT = 1 + 1 * random.nextDouble();
|
|
|
+ p.fastP = 2 * Math.PI * random.nextDouble();
|
|
|
+ p.availability = 99 + random.nextDouble();
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 太阳天顶角余弦近似: 夜间为0, 正午(墙钟12点)为1, 白天呈正弦钟形
|
|
|
+ */
|
|
|
+ private static double coszAt(DayPhysics p, ZonedDateTime t)
|
|
|
+ {
|
|
|
+ double hour = t.getHour() + t.getMinute() / 60.0;
|
|
|
+ if (hour <= p.sunrise || hour >= p.sunset)
|
|
|
+ {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ return Math.sin(Math.PI * (hour - p.sunrise) / (p.sunset - p.sunrise));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 指定epoch时刻的总云量(0~100), 慢漂移
|
|
|
+ */
|
|
|
+ private static double tccAt(DayPhysics p, double tH)
|
|
|
+ {
|
|
|
+ return clamp(p.baseCc + p.ampCc * Math.sin(2 * Math.PI * tH / p.tCc + p.pCc), 0, 100);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 指定时刻的水平面总辐照度GHI(W/m²):
|
|
|
+ * 晴空峰值随季节, 云量主要削弱直分量, 散射随云量增大, 夜间为0
|
|
|
+ */
|
|
|
+ private static double ghiAt(DayPhysics p, ZonedDateTime t)
|
|
|
+ {
|
|
|
+ double cosz = coszAt(p, t);
|
|
|
+ if (cosz <= 0)
|
|
|
+ {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ double peak = 750 + 100 * p.season;
|
|
|
+ double cloud = tccAt(p, t.toInstant().getEpochSecond() / 3600.0) / 100.0;
|
|
|
+ return 0.90 * peak * (1 - 0.95 * cloud) * cosz + 0.10 * peak * cosz * (1 + cloud);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 当日当地00:00至tm的水平面总辐照度累计(MJ/m²),
|
|
|
+ * 按5分钟步长用当日物理参数积分
|
|
|
+ */
|
|
|
+ private static double dailyIrradianceMj(DayPhysics p, ZonedDateTime tm)
|
|
|
+ {
|
|
|
+ ZonedDateTime day0 = tm.toLocalDate().atStartOfDay(SHANGHAI);
|
|
|
+ double mj = 0;
|
|
|
+ for (long s = 0; s < tm.getHour() * 3600L + tm.getMinute() * 60L; s += 300)
|
|
|
+ {
|
|
|
+ mj += ghiAt(p, day0.plusSeconds(s)) * 300.0 / 1e6;
|
|
|
+ }
|
|
|
+ return mj;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按指定小数位四舍五入
|
|
|
+ */
|
|
|
+ private static double round(double v, int scale)
|
|
|
+ {
|
|
|
+ double f = Math.pow(10, scale);
|
|
|
+ return Math.round(v * f) / f;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 采集数据mock: 每北京日期一组物理参数(钟形/云量/气压/风/快云/可用率)
|
|
|
+ */
|
|
|
+ private static final class DayPhysics
|
|
|
+ {
|
|
|
+ /** 季节系数(-1冬至~1夏至) */
|
|
|
+ double season;
|
|
|
+ /** 日出时刻(北京墙钟小时) */
|
|
|
+ double sunrise;
|
|
|
+ /** 日落时刻(北京墙钟小时) */
|
|
|
+ double sunset;
|
|
|
+ /** 慢云量基准(%) */
|
|
|
+ double baseCc;
|
|
|
+ /** 慢云量幅值(%) */
|
|
|
+ double ampCc;
|
|
|
+ /** 慢云量周期(小时) */
|
|
|
+ double tCc;
|
|
|
+ /** 慢云量相位(rad) */
|
|
|
+ double pCc;
|
|
|
+ /** 气压漂移周期(小时) */
|
|
|
+ double tP;
|
|
|
+ /** 气压漂移相位(rad) */
|
|
|
+ double pP;
|
|
|
+ /** 风速基准(m/s) */
|
|
|
+ double windBase;
|
|
|
+ /** 风速日变化相位(rad) */
|
|
|
+ double pW;
|
|
|
+ /** 风向基准(度) */
|
|
|
+ double dirBase;
|
|
|
+ /** 风向慢摆周期(小时) */
|
|
|
+ double tD;
|
|
|
+ /** 风向慢摆相位(rad) */
|
|
|
+ double pD;
|
|
|
+ /** 风向快摆周期(小时) */
|
|
|
+ double tD2;
|
|
|
+ /** 风向快摆相位(rad) */
|
|
|
+ double pD2;
|
|
|
+ /** 快云量幅值(功率衰减系数) */
|
|
|
+ double fastAmp;
|
|
|
+ /** 快云量周期(小时) */
|
|
|
+ double fastT;
|
|
|
+ /** 快云量相位(rad) */
|
|
|
+ double fastP;
|
|
|
+ /** 可用率(%) */
|
|
|
+ double availability;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 生成单个mock预报文件
|
|
|
*
|