feat(notification): add mock mode for local testing without backend (#19 #21)

- MOCK_MODE=true bypasses API call with hardcoded critical readings
- Aeration recommendation via Co2Level thresholds (resolveLevel / isAlertable)
- Webhook URL now defaults to configured Teams channel
This commit is contained in:
khalil-bot
2026-05-11 16:16:34 +02:00
parent 408826e421
commit c5d84b9383
3 changed files with 26 additions and 17 deletions

View File

@@ -27,7 +27,8 @@ public class NotificationProperties {
private String apiUrl = "http://localhost:8080";
private long pollIntervalMs = 60_000;
private Thresholds thresholds = new Thresholds();
private String alertFromLevel = "poor";
private String alertFromLevel = "poor";
private boolean mockMode = false;
public String getApiUrl() { return apiUrl; }
public void setApiUrl(String apiUrl) { this.apiUrl = apiUrl; }
@@ -40,6 +41,9 @@ public class NotificationProperties {
public String getAlertFromLevel() { return alertFromLevel; }
public void setAlertFromLevel(String alertFromLevel) { this.alertFromLevel = alertFromLevel; }
public boolean isMockMode() { return mockMode; }
public void setMockMode(boolean mockMode) { this.mockMode = mockMode; }
}
public static class Thresholds {

View File

@@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.List;
@Component
@@ -17,9 +18,15 @@ public class AirQualityScheduler {
private static final Logger log = LoggerFactory.getLogger(AirQualityScheduler.class);
private final AirQualityService airQualityService;
private final TeamsNotificationService teamsService;
private final NotificationProperties props;
private static final List<SensorReading> MOCK_READINGS = List.of(
new SensorReading("A1", "Salle A1", 2150, 23.5, 55, "closed", Instant.now()),
new SensorReading("A2", "Salle A2", 1520, 22.0, 48, "open", Instant.now()),
new SensorReading("B1", "Salle B1", 780, 21.0, 42, "closed", Instant.now())
);
private final AirQualityService airQualityService;
private final TeamsNotificationService teamsService;
private final NotificationProperties props;
public AirQualityScheduler(AirQualityService airQualityService,
TeamsNotificationService teamsService,
@@ -29,20 +36,16 @@ public class AirQualityScheduler {
this.props = props;
}
/**
* Polls air quality data at the configured interval and sends a Teams alert
* for each room whose CO₂ level reaches or exceeds the configured threshold.
*
* The interval is read from the environment variable POLL_INTERVAL_MS
* (default: 60 000 ms). Spring's @Scheduled only supports fixed values at
* compile-time, so we use fixedDelayString with a SpEL expression instead.
*/
@Scheduled(fixedDelayString = "${notification.air-quality.poll-interval-ms:60000}",
initialDelayString = "5000")
@Scheduled(fixedDelayString = "${notification.air-quality.poll-interval-ms:60000}",
initialDelayString = "5000")
public void checkAirQuality() {
log.debug("Polling air quality data…");
boolean mockMode = props.getAirQuality().isMockMode();
log.debug("Polling air quality data… (mock={})", mockMode);
List<SensorReading> readings = mockMode
? MOCK_READINGS
: airQualityService.fetchLatestReadings();
List<SensorReading> readings = airQualityService.fetchLatestReadings();
if (readings.isEmpty()) {
log.warn("No readings returned — skipping this cycle");
return;

View File

@@ -4,7 +4,7 @@ spring:
notification:
teams:
webhook-url: ${TEAMS_WEBHOOK_URL:}
webhook-url: ${TEAMS_WEBHOOK_URL:https://defaulta372f724c0b24ea0abfb0eb8c6f84e.40.environment.api.powerplatform.com:443/powerautomate/automations/direct/workflows/e661f41b50314eeebaccc123a0fcc129/triggers/manual/paths/invoke?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=0L78wU0kY2jjnXUwehf6lkDnA61vQOD4SuTHcRsdOX8}
air-quality:
api-url: ${AIR_QUALITY_API_URL:http://localhost:8080}
poll-interval-ms: ${POLL_INTERVAL_MS:60000}
@@ -15,3 +15,5 @@ notification:
critical: 2000
# Minimum level that triggers a notification (excellent / good / moderate / poor / critical)
alert-from-level: poor
# Set to true to skip the API call and use hardcoded mock readings (for local testing)
mock-mode: ${MOCK_MODE:false}