From cb63d6a2370ccf670a7856301f1530b52d67c8b6 Mon Sep 17 00:00:00 2001 From: litoral05 Date: Fri, 22 May 2026 13:36:27 +0100 Subject: [PATCH] Add meteo module API and websocket stream --- .../AcquisitionSchedulerService.java | 7 +- .../modules/meteo/MeteoModuleController.java | 19 +++++ .../modules/meteo/MeteoModuleResponse.java | 13 +++ .../modules/meteo/MeteoModuleService.java | 82 +++++++++++++++++++ .../MeteoModuleWebSocketPublisher.java | 29 +++++++ .../modules/shared/ModuleSensorResponse.java | 15 ++++ 6 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/litoralregas/backend/modules/meteo/MeteoModuleController.java create mode 100644 src/main/java/com/litoralregas/backend/modules/meteo/MeteoModuleResponse.java create mode 100644 src/main/java/com/litoralregas/backend/modules/meteo/MeteoModuleService.java create mode 100644 src/main/java/com/litoralregas/backend/modules/meteo/websocket/MeteoModuleWebSocketPublisher.java create mode 100644 src/main/java/com/litoralregas/backend/modules/shared/ModuleSensorResponse.java diff --git a/src/main/java/com/litoralregas/backend/acquisition/scheduler/AcquisitionSchedulerService.java b/src/main/java/com/litoralregas/backend/acquisition/scheduler/AcquisitionSchedulerService.java index 25becb6..5c8ea06 100644 --- a/src/main/java/com/litoralregas/backend/acquisition/scheduler/AcquisitionSchedulerService.java +++ b/src/main/java/com/litoralregas/backend/acquisition/scheduler/AcquisitionSchedulerService.java @@ -2,6 +2,7 @@ package com.litoralregas.backend.acquisition.scheduler; import com.litoralregas.backend.acquisition.polling.AcquisitionPollResult; import com.litoralregas.backend.acquisition.block.BlockPollingService; +import com.litoralregas.backend.modules.meteo.websocket.MeteoModuleWebSocketPublisher; import com.litoralregas.backend.websocket.dashboard.DashboardOverviewWebSocketPublisher; import com.litoralregas.backend.websocket.telemetry.TelemetryWebSocketPublisher; import jakarta.annotation.PostConstruct; @@ -20,6 +21,7 @@ public class AcquisitionSchedulerService { private final TaskScheduler taskScheduler; private final TelemetryWebSocketPublisher telemetryWebSocketPublisher; private final DashboardOverviewWebSocketPublisher dashboardOverviewWebSocketPublisher; + private final MeteoModuleWebSocketPublisher meteoModuleWebSocketPublisher; private final AcquisitionRuntimeStatus runtimeStatus = new AcquisitionRuntimeStatus(); @@ -30,13 +32,15 @@ public class AcquisitionSchedulerService { AcquisitionSchedulerProperties properties, @Qualifier("acquisitionTaskScheduler") TaskScheduler taskScheduler, TelemetryWebSocketPublisher telemetryWebSocketPublisher, - DashboardOverviewWebSocketPublisher dashboardOverviewWebSocketPublisher + DashboardOverviewWebSocketPublisher dashboardOverviewWebSocketPublisher, + MeteoModuleWebSocketPublisher meteoModuleWebSocketPublisher ) { this.blockPollingService = blockPollingService; this.properties = properties; this.taskScheduler = taskScheduler; this.telemetryWebSocketPublisher = telemetryWebSocketPublisher; this.dashboardOverviewWebSocketPublisher = dashboardOverviewWebSocketPublisher; + this.meteoModuleWebSocketPublisher = meteoModuleWebSocketPublisher; } @PostConstruct @@ -76,6 +80,7 @@ public class AcquisitionSchedulerService { telemetryWebSocketPublisher.publishLatestTelemetry(); dashboardOverviewWebSocketPublisher.publishOverview(); + meteoModuleWebSocketPublisher.publishLatest(); } catch (Exception exception) { runtimeStatus.setLastError(exception.getMessage()); diff --git a/src/main/java/com/litoralregas/backend/modules/meteo/MeteoModuleController.java b/src/main/java/com/litoralregas/backend/modules/meteo/MeteoModuleController.java new file mode 100644 index 0000000..7de4a23 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/modules/meteo/MeteoModuleController.java @@ -0,0 +1,19 @@ +package com.litoralregas.backend.modules.meteo; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class MeteoModuleController { + + private final MeteoModuleService meteoModuleService; + + public MeteoModuleController(MeteoModuleService meteoModuleService) { + this.meteoModuleService = meteoModuleService; + } + + @GetMapping("/api/modules/meteo") + public MeteoModuleResponse getLatest() { + return meteoModuleService.getLatest(); + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/modules/meteo/MeteoModuleResponse.java b/src/main/java/com/litoralregas/backend/modules/meteo/MeteoModuleResponse.java new file mode 100644 index 0000000..2cd4af9 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/modules/meteo/MeteoModuleResponse.java @@ -0,0 +1,13 @@ +package com.litoralregas.backend.modules.meteo; + +import com.litoralregas.backend.modules.shared.ModuleSensorResponse; + +import java.time.Instant; +import java.util.List; + +public record MeteoModuleResponse( + Instant timestamp, + Integer sensorCount, + List sensors +) { +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/modules/meteo/MeteoModuleService.java b/src/main/java/com/litoralregas/backend/modules/meteo/MeteoModuleService.java new file mode 100644 index 0000000..d2b7ae5 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/modules/meteo/MeteoModuleService.java @@ -0,0 +1,82 @@ +package com.litoralregas.backend.modules.meteo; + +import com.litoralregas.backend.acquisition.telemetry.TelemetryCache; +import com.litoralregas.backend.acquisition.telemetry.TelemetrySnapshot; +import com.litoralregas.backend.modules.shared.ModuleSensorResponse; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.Comparator; +import java.util.List; + +@Service +public class MeteoModuleService { + + private final TelemetryCache telemetryCache; + + public MeteoModuleService(TelemetryCache telemetryCache) { + this.telemetryCache = telemetryCache; + } + + public MeteoModuleResponse getLatest() { + List sensors = telemetryCache.getAll() + .stream() + .filter(this::isMeteoSensor) + .sorted(Comparator.comparing(TelemetrySnapshot::sensorId)) + .map(this::toResponse) + .toList(); + + return new MeteoModuleResponse( + Instant.now(), + sensors.size(), + sensors + ); + } + + private boolean isMeteoSensor(TelemetrySnapshot snapshot) { + String name = normalize(snapshot.name()); + + return name.contains("exterior") + || name.contains("vento") + || name.contains("radiacao") + || name.contains("chuva"); + } + + private ModuleSensorResponse toResponse(TelemetrySnapshot snapshot) { + return new ModuleSensorResponse( + snapshot.sensorId(), + snapshot.name(), + buildKey(snapshot.name()), + snapshot.value(), + snapshot.unit(), + snapshot.modbusAddress(), + snapshot.bitOffset(), + snapshot.timestamp() + ); + } + + private String buildKey(String name) { + return normalize(name) + .replaceAll("[^a-z0-9]+", ".") + .replaceAll("^\\.|\\.$", ""); + } + + private String normalize(String value) { + if (value == null) { + return ""; + } + + return value + .toLowerCase() + .replace("ç", "c") + .replace("ã", "a") + .replace("á", "a") + .replace("à", "a") + .replace("é", "e") + .replace("ê", "e") + .replace("í", "i") + .replace("ó", "o") + .replace("õ", "o") + .replace("ú", "u"); + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/modules/meteo/websocket/MeteoModuleWebSocketPublisher.java b/src/main/java/com/litoralregas/backend/modules/meteo/websocket/MeteoModuleWebSocketPublisher.java new file mode 100644 index 0000000..e580631 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/modules/meteo/websocket/MeteoModuleWebSocketPublisher.java @@ -0,0 +1,29 @@ +package com.litoralregas.backend.modules.meteo.websocket; + +import com.litoralregas.backend.modules.meteo.MeteoModuleResponse; +import com.litoralregas.backend.modules.meteo.MeteoModuleService; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Service; + +@Service +public class MeteoModuleWebSocketPublisher { + + private static final String DESTINATION = "/topic/modules/meteo/latest"; + + private final SimpMessagingTemplate messagingTemplate; + private final MeteoModuleService meteoModuleService; + + public MeteoModuleWebSocketPublisher( + SimpMessagingTemplate messagingTemplate, + MeteoModuleService meteoModuleService + ) { + this.messagingTemplate = messagingTemplate; + this.meteoModuleService = meteoModuleService; + } + + public void publishLatest() { + System.out.println("Publishing meteo module websocket"); + MeteoModuleResponse response = meteoModuleService.getLatest(); + messagingTemplate.convertAndSend(DESTINATION, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/modules/shared/ModuleSensorResponse.java b/src/main/java/com/litoralregas/backend/modules/shared/ModuleSensorResponse.java new file mode 100644 index 0000000..2af1b28 --- /dev/null +++ b/src/main/java/com/litoralregas/backend/modules/shared/ModuleSensorResponse.java @@ -0,0 +1,15 @@ +package com.litoralregas.backend.modules.shared; + +import java.time.Instant; + +public record ModuleSensorResponse( + Integer sensorId, + String name, + String key, + Object value, + String unit, + Integer modbusAddress, + Integer bitOffset, + Instant timestamp +) { +} \ No newline at end of file