Add meteo module API and websocket stream

This commit is contained in:
litoral05
2026-05-22 13:36:27 +01:00
parent 5e30160b31
commit cb63d6a237
6 changed files with 164 additions and 1 deletions
@@ -2,6 +2,7 @@ package com.litoralregas.backend.acquisition.scheduler;
import com.litoralregas.backend.acquisition.polling.AcquisitionPollResult; import com.litoralregas.backend.acquisition.polling.AcquisitionPollResult;
import com.litoralregas.backend.acquisition.block.BlockPollingService; 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.dashboard.DashboardOverviewWebSocketPublisher;
import com.litoralregas.backend.websocket.telemetry.TelemetryWebSocketPublisher; import com.litoralregas.backend.websocket.telemetry.TelemetryWebSocketPublisher;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
@@ -20,6 +21,7 @@ public class AcquisitionSchedulerService {
private final TaskScheduler taskScheduler; private final TaskScheduler taskScheduler;
private final TelemetryWebSocketPublisher telemetryWebSocketPublisher; private final TelemetryWebSocketPublisher telemetryWebSocketPublisher;
private final DashboardOverviewWebSocketPublisher dashboardOverviewWebSocketPublisher; private final DashboardOverviewWebSocketPublisher dashboardOverviewWebSocketPublisher;
private final MeteoModuleWebSocketPublisher meteoModuleWebSocketPublisher;
private final AcquisitionRuntimeStatus runtimeStatus = new AcquisitionRuntimeStatus(); private final AcquisitionRuntimeStatus runtimeStatus = new AcquisitionRuntimeStatus();
@@ -30,13 +32,15 @@ public class AcquisitionSchedulerService {
AcquisitionSchedulerProperties properties, AcquisitionSchedulerProperties properties,
@Qualifier("acquisitionTaskScheduler") TaskScheduler taskScheduler, @Qualifier("acquisitionTaskScheduler") TaskScheduler taskScheduler,
TelemetryWebSocketPublisher telemetryWebSocketPublisher, TelemetryWebSocketPublisher telemetryWebSocketPublisher,
DashboardOverviewWebSocketPublisher dashboardOverviewWebSocketPublisher DashboardOverviewWebSocketPublisher dashboardOverviewWebSocketPublisher,
MeteoModuleWebSocketPublisher meteoModuleWebSocketPublisher
) { ) {
this.blockPollingService = blockPollingService; this.blockPollingService = blockPollingService;
this.properties = properties; this.properties = properties;
this.taskScheduler = taskScheduler; this.taskScheduler = taskScheduler;
this.telemetryWebSocketPublisher = telemetryWebSocketPublisher; this.telemetryWebSocketPublisher = telemetryWebSocketPublisher;
this.dashboardOverviewWebSocketPublisher = dashboardOverviewWebSocketPublisher; this.dashboardOverviewWebSocketPublisher = dashboardOverviewWebSocketPublisher;
this.meteoModuleWebSocketPublisher = meteoModuleWebSocketPublisher;
} }
@PostConstruct @PostConstruct
@@ -76,6 +80,7 @@ public class AcquisitionSchedulerService {
telemetryWebSocketPublisher.publishLatestTelemetry(); telemetryWebSocketPublisher.publishLatestTelemetry();
dashboardOverviewWebSocketPublisher.publishOverview(); dashboardOverviewWebSocketPublisher.publishOverview();
meteoModuleWebSocketPublisher.publishLatest();
} catch (Exception exception) { } catch (Exception exception) {
runtimeStatus.setLastError(exception.getMessage()); runtimeStatus.setLastError(exception.getMessage());
@@ -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();
}
}
@@ -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<ModuleSensorResponse> sensors
) {
}
@@ -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<ModuleSensorResponse> 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");
}
}
@@ -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);
}
}
@@ -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
) {
}