diff --git a/notification_service/Dockerfile b/notification_service/Dockerfile new file mode 100644 index 0000000..2772187 --- /dev/null +++ b/notification_service/Dockerfile @@ -0,0 +1,20 @@ +# ── Stage 1 : build ────────────────────────────────────────────────────────── +FROM eclipse-temurin:17-jdk-alpine AS builder +WORKDIR /app + +COPY pom.xml . +COPY src ./src + +RUN apk add --no-cache maven && mvn package -DskipTests -q + +# ── Stage 2 : run ───────────────────────────────────────────────────────────── +FROM eclipse-temurin:17-jre-alpine +WORKDIR /app + +COPY --from=builder /app/target/notification-service-*.jar app.jar + +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD wget -qO- http://localhost:8080/actuator/health || exit 1 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/notification_service/pom.xml b/notification_service/pom.xml index 1c83f27..aff6c17 100644 --- a/notification_service/pom.xml +++ b/notification_service/pom.xml @@ -37,6 +37,10 @@ spring-dotenv 4.0.0 + + org.springframework.boot + spring-boot-starter-actuator + org.springframework.boot diff --git a/notification_service/src/main/java/ch/hesso/pi/notification/scheduler/AirQualityScheduler.java b/notification_service/src/main/java/ch/hesso/pi/notification/scheduler/AirQualityScheduler.java index 2ff8b47..5693052 100644 --- a/notification_service/src/main/java/ch/hesso/pi/notification/scheduler/AirQualityScheduler.java +++ b/notification_service/src/main/java/ch/hesso/pi/notification/scheduler/AirQualityScheduler.java @@ -12,6 +12,8 @@ import org.springframework.stereotype.Component; import java.time.Instant; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; @Component public class AirQualityScheduler { @@ -28,6 +30,9 @@ public class AirQualityScheduler { private final TelegramNotificationService telegramService; private final NotificationProperties props; + // last alerted level per room — null means no alert is active for that room + private final Map lastAlertedLevel = new ConcurrentHashMap<>(); + public AirQualityScheduler(AirQualityService airQualityService, TelegramNotificationService telegramService, NotificationProperties props) { @@ -36,8 +41,8 @@ public class AirQualityScheduler { this.props = props; } - @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() { boolean mockMode = props.getAirQuality().isMockMode(); log.debug("Polling air quality data… (mock={})", mockMode); @@ -53,9 +58,19 @@ public class AirQualityScheduler { for (SensorReading reading : readings) { Co2Level level = airQualityService.resolveLevel(reading.co2()); + if (airQualityService.isAlertable(level)) { - log.info("Alertable reading: room={} co2={} level={}", reading.roomId(), reading.co2(), level); - telegramService.sendAlert(reading, level); + Co2Level previous = lastAlertedLevel.get(reading.roomId()); + if (level != previous) { + log.info("Alert level changed: room={} {} -> {}", reading.roomId(), previous, level); + telegramService.sendAlert(reading, level); + lastAlertedLevel.put(reading.roomId(), level); + } + } else { + // room recovered — reset so next alert triggers a new notification + if (lastAlertedLevel.remove(reading.roomId()) != null) { + log.info("Room {} recovered to {}", reading.roomId(), level); + } } } } diff --git a/notification_service/src/main/resources/application.yml b/notification_service/src/main/resources/application.yml index 162d42e..66c7e93 100644 --- a/notification_service/src/main/resources/application.yml +++ b/notification_service/src/main/resources/application.yml @@ -2,6 +2,15 @@ spring: application: name: notification-service +management: + endpoints: + web: + exposure: + include: health + endpoint: + health: + show-details: never + notification: telegram: bot-token: ${TELEGRAM_BOT_TOKEN}