Add accumulated analytics with radiation integration
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
package com.litoralregas.backend.historian;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
public record HistorianAccumulatedBucket(
|
||||||
|
String label,
|
||||||
|
Instant from,
|
||||||
|
Instant to,
|
||||||
|
Double total,
|
||||||
|
String unit
|
||||||
|
) {}
|
||||||
@@ -46,4 +46,30 @@ public class HistorianController {
|
|||||||
) {
|
) {
|
||||||
return historianService.getDashboardHistory(keys, from, to);
|
return historianService.getDashboardHistory(keys, from, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/api/historian/accumulated")
|
||||||
|
public List<HistorianAccumulatedBucket> getAccumulated(
|
||||||
|
@RequestParam String key,
|
||||||
|
@RequestParam String range
|
||||||
|
) {
|
||||||
|
Instant to = Instant.now();
|
||||||
|
Instant from;
|
||||||
|
String bucket = "day";
|
||||||
|
|
||||||
|
switch (range) {
|
||||||
|
case "30d" -> from = to.minus(java.time.Duration.ofDays(30));
|
||||||
|
case "month" -> {
|
||||||
|
java.time.ZonedDateTime now = java.time.ZonedDateTime.now(java.time.ZoneId.of("Europe/Lisbon"));
|
||||||
|
from = now.withDayOfMonth(1).truncatedTo(java.time.temporal.ChronoUnit.DAYS).toInstant();
|
||||||
|
}
|
||||||
|
case "year" -> {
|
||||||
|
java.time.ZonedDateTime now = java.time.ZonedDateTime.now(java.time.ZoneId.of("Europe/Lisbon"));
|
||||||
|
from = now.withDayOfYear(1).truncatedTo(java.time.temporal.ChronoUnit.DAYS).toInstant();
|
||||||
|
bucket = "month";
|
||||||
|
}
|
||||||
|
default -> from = to.minus(java.time.Duration.ofDays(7));
|
||||||
|
}
|
||||||
|
|
||||||
|
return historianService.getAccumulated(key, from, to, bucket);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -183,4 +183,143 @@ public class HistorianService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<HistorianAccumulatedBucket> getAccumulated(
|
||||||
|
String keyName,
|
||||||
|
Instant from,
|
||||||
|
Instant to,
|
||||||
|
String bucket
|
||||||
|
) {
|
||||||
|
List<HistorianSample> samples = historianSampleRepository
|
||||||
|
.findByKeyNameAndSampledAtBetweenOrderBySampledAtAsc(keyName, from, to);
|
||||||
|
|
||||||
|
if (samples.isEmpty()) return List.of();
|
||||||
|
|
||||||
|
Map<Instant, List<HistorianSample>> grouped = samples.stream()
|
||||||
|
.collect(java.util.stream.Collectors.groupingBy(
|
||||||
|
sample -> bucketStart(sample.getSampledAt(), bucket),
|
||||||
|
LinkedHashMap::new,
|
||||||
|
java.util.stream.Collectors.toList()
|
||||||
|
));
|
||||||
|
|
||||||
|
return grouped.entrySet().stream()
|
||||||
|
.map(entry -> {
|
||||||
|
Instant bucketFrom = entry.getKey();
|
||||||
|
Instant bucketTo = bucketEnd(bucketFrom, bucket);
|
||||||
|
|
||||||
|
List<HistorianSample> bucketSamples = entry.getValue();
|
||||||
|
|
||||||
|
String unit;
|
||||||
|
double total;
|
||||||
|
|
||||||
|
if (isRadiationKey(keyName)) {
|
||||||
|
total = integrateWhPerSquareMeter(bucketSamples);
|
||||||
|
unit = "Wh/m²";
|
||||||
|
} else {
|
||||||
|
total = bucketSamples.stream()
|
||||||
|
.map(HistorianSample::getNumericValue)
|
||||||
|
.filter(java.util.Objects::nonNull)
|
||||||
|
.mapToDouble(Double::doubleValue)
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
unit = bucketSamples.stream()
|
||||||
|
.map(HistorianSample::getUnit)
|
||||||
|
.filter(java.util.Objects::nonNull)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HistorianAccumulatedBucket(
|
||||||
|
bucketLabel(bucketFrom, bucket),
|
||||||
|
bucketFrom,
|
||||||
|
bucketTo,
|
||||||
|
total,
|
||||||
|
unit
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRadiationKey(String keyName) {
|
||||||
|
String normalized = keyName.toLowerCase();
|
||||||
|
|
||||||
|
return normalized.contains("radiacao")
|
||||||
|
|| normalized.contains("radiação")
|
||||||
|
|| normalized.contains("radiation");
|
||||||
|
}
|
||||||
|
|
||||||
|
private double integrateWhPerSquareMeter(List<HistorianSample> samples) {
|
||||||
|
if (samples.size() < 2) return 0.0;
|
||||||
|
|
||||||
|
double total = 0.0;
|
||||||
|
|
||||||
|
for (int i = 1; i < samples.size(); i++) {
|
||||||
|
HistorianSample previous = samples.get(i - 1);
|
||||||
|
HistorianSample current = samples.get(i);
|
||||||
|
|
||||||
|
if (previous.getNumericValue() == null || current.getNumericValue() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double previousValue = previous.getNumericValue();
|
||||||
|
double currentValue = current.getNumericValue();
|
||||||
|
|
||||||
|
double averageWm2 = (previousValue + currentValue) / 2.0;
|
||||||
|
|
||||||
|
double elapsedHours =
|
||||||
|
java.time.Duration.between(
|
||||||
|
previous.getSampledAt(),
|
||||||
|
current.getSampledAt()
|
||||||
|
).toMillis() / 1000.0 / 60.0 / 60.0;
|
||||||
|
|
||||||
|
if (elapsedHours <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
total += averageWm2 * elapsedHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Instant bucketStart(Instant instant, String bucket) {
|
||||||
|
java.time.ZonedDateTime date = instant.atZone(java.time.ZoneId.of("Europe/Lisbon"));
|
||||||
|
|
||||||
|
return switch (bucket) {
|
||||||
|
case "month" -> date
|
||||||
|
.withDayOfMonth(1)
|
||||||
|
.truncatedTo(java.time.temporal.ChronoUnit.DAYS)
|
||||||
|
.toInstant();
|
||||||
|
|
||||||
|
case "year" -> date
|
||||||
|
.withDayOfYear(1)
|
||||||
|
.truncatedTo(java.time.temporal.ChronoUnit.DAYS)
|
||||||
|
.toInstant();
|
||||||
|
|
||||||
|
default -> date
|
||||||
|
.truncatedTo(java.time.temporal.ChronoUnit.DAYS)
|
||||||
|
.toInstant();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Instant bucketEnd(Instant bucketFrom, String bucket) {
|
||||||
|
java.time.ZonedDateTime date = bucketFrom.atZone(java.time.ZoneId.of("Europe/Lisbon"));
|
||||||
|
|
||||||
|
return switch (bucket) {
|
||||||
|
case "month" -> date.plusMonths(1).toInstant();
|
||||||
|
case "year" -> date.plusYears(1).toInstant();
|
||||||
|
default -> date.plusDays(1).toInstant();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String bucketLabel(Instant bucketFrom, String bucket) {
|
||||||
|
java.time.ZonedDateTime date = bucketFrom.atZone(java.time.ZoneId.of("Europe/Lisbon"));
|
||||||
|
|
||||||
|
return switch (bucket) {
|
||||||
|
case "month" -> date.format(java.time.format.DateTimeFormatter.ofPattern("MM/yyyy"));
|
||||||
|
case "year" -> String.valueOf(date.getYear());
|
||||||
|
default -> date.format(java.time.format.DateTimeFormatter.ofPattern("dd/MM"));
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user