Add accumulated analytics with radiation integration

This commit is contained in:
litoral05
2026-05-22 17:23:36 +01:00
parent db75744305
commit af709852ac
3 changed files with 176 additions and 0 deletions
@@ -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"));
};
}
}