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}