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);
|
||||
}
|
||||
|
||||
@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