2026年8月3日 星期一

[水井村USR] EP07|智慧養殖警示系統實戰:水質異常即時通知

智慧生活科技專業社群|IoT 入門系列|Email × LINE Messaging API × App 推播
前一篇已完成 Django Dashboard,即時呈現水溫、pH、溶氧與鹽度。EP07 將進一步建立「主動警示」:當水質超出設定範圍,Django 自動建立警示紀錄,並透過 Email、LINE Messaging API 或 Firebase Cloud Messaging 將訊息送到管理者手機。

請先將 EP07 教學資訊圖上傳 Blogger,再把 EP07_MAIN_IMAGE_URL 換成圖片網址。

圖:水質感測、Django 異常判斷與多管道通知的完整流程。
版本提醒:資訊圖中的「LINE Notify」屬於舊版架構;LINE Notify 服務已停止,本文改採 LINE Official Account 的 Messaging API Push Message

一、為什麼 Dashboard 還不夠?

Dashboard 必須由使用者主動開啟。如果養殖戶正在工作、休息或不在電腦旁,可能無法立即發現異常。因此警示系統應具備:

快速發現新資料寫入後立即檢查。
主動通知系統主動傳送 Email、LINE 或 App 推播。
保留紀錄保存異常類型、數值、時間及處理狀態。
避免洗版使用冷卻時間與恢復通知。
感測資料 Django REST API 異常判斷 建立警示紀錄 Email/LINE/FCM

二、先定義警示門檻

項目示範正常範圍示範異常條件
水溫18~32°C低於 18°C 或高於 32°C
pH6.0~8.5低於 6.0 或高於 8.5
溶氧 DO高於 4.0 mg/L低於 4.0 mg/L
導電度 EC200~2,000 µS/cm低於 200 或高於 2,000
重要:以上門檻僅供程式教學。正式使用時,必須依養殖物種、生命階段、鹽度、季節、放養密度與專業養殖建議設定。

三、建立警示資料模型

models.py
from django.db import models

class Alert(models.Model):
    LEVEL_CHOICES = [
        ("INFO", "資訊"),
        ("WARNING", "警告"),
        ("CRITICAL", "嚴重"),
    ]

    pond = models.ForeignKey(
        "Pond",
        on_delete=models.CASCADE,
        related_name="alerts"
    )

    reading = models.ForeignKey(
        "SensorReading",
        on_delete=models.CASCADE,
        related_name="alerts"
    )

    alert_type = models.CharField(
        max_length=50
    )

    message = models.CharField(
        max_length=300
    )

    level = models.CharField(
        max_length=10,
        choices=LEVEL_CHOICES
    )

    is_read = models.BooleanField(
        default=False
    )

    is_resolved = models.BooleanField(
        default=False
    )

    created_at = models.DateTimeField(
        auto_now_add=True
    )

    resolved_at = models.DateTimeField(
        null=True,
        blank=True
    )

    class Meta:
        ordering = ["-created_at"]

警示紀錄除了保存訊息,也應保存是否已讀、是否解除,以及對應的原始感測資料。

四、建立異常判斷函式

services/alert_service.py
def check_abnormal(reading):
    alerts = []

    temperature = reading.water_temperature
    ph = reading.ph
    do = reading.dissolved_oxygen
    salinity = reading.salinity

    if temperature is not None:
        if temperature > 32:
            alerts.append({
                "type": "HIGH_TEMPERATURE",
                "level": "WARNING",
                "message":
                    f"水溫過高:{temperature:.2f}°C"
            })

        elif temperature < 18:
            alerts.append({
                "type": "LOW_TEMPERATURE",
                "level": "WARNING",
                "message":
                    f"水溫過低:{temperature:.2f}°C"
            })

    if ph is not None and (
        ph > 8.5 or ph < 6.0
    ):
        alerts.append({
            "type": "PH_ABNORMAL",
            "level": "WARNING",
            "message":
                f"pH 異常:{ph:.2f}"
        })

    if do is not None and do < 4.0:
        alerts.append({
            "type": "LOW_DO",
            "level": "CRITICAL",
            "message":
                f"溶氧過低:{do:.2f} mg/L"
        })

    return alerts

五、感測資料寫入後觸發警示

APIView 概念
@api_view(["POST"])
def sensor_reading(request):
    serializer = SensorReadingSerializer(
        data=request.data
    )

    if not serializer.is_valid():
        return Response(
            serializer.errors,
            status=400
        )

    reading = serializer.save()

    alert_items = check_abnormal(reading)

    for item in alert_items:
        alert = Alert.objects.create(
            pond=reading.pond,
            reading=reading,
            alert_type=item["type"],
            level=item["level"],
            message=item["message"]
        )

        dispatch_alert(alert)

    return Response(
        serializer.data,
        status=201
    )
異常判斷放在 Django 端而不是 ESP32,可以統一修改門檻、管理通知方式,且不需要重新燒錄現場設備。

六、Email 通知

settings.py
EMAIL_BACKEND =
    "django.core.mail.backends.smtp.EmailBackend"

EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 587
EMAIL_USE_TLS = True

EMAIL_HOST_USER =
    "your_account@gmail.com"

EMAIL_HOST_PASSWORD =
    "請使用應用程式密碼"
發送 Email
from django.conf import settings
from django.core.mail import send_mail

def send_alert_email(alert):
    subject = (
        f"【智慧養殖警示】"
        f"{alert.pond}-{alert.alert_type}"
    )

    message = (
        f"魚塭:{alert.pond}\n"
        f"等級:{alert.level}\n"
        f"訊息:{alert.message}\n"
        f"時間:{alert.created_at}\n"
    )

    send_mail(
        subject,
        message,
        settings.DEFAULT_FROM_EMAIL,
        ["manager@example.com"],
        fail_silently=False
    )

七、使用 LINE Messaging API 推播

使用 LINE Official Account 的 Messaging API,可以向已符合推播條件的使用者或群組送出訊息。

LINE Push Message
import requests
from django.conf import settings

def send_line_message(alert, user_id):
    url = (
        "https://api.line.me/"
        "v2/bot/message/push"
    )

    headers = {
        "Authorization":
            f"Bearer {settings.LINE_CHANNEL_ACCESS_TOKEN}",

        "Content-Type":
            "application/json"
    }

    body = {
        "to": user_id,
        "messages": [
            {
                "type": "text",
                "text": (
                    "【智慧養殖警示】\n"
                    f"魚塭:{alert.pond}\n"
                    f"等級:{alert.level}\n"
                    f"訊息:{alert.message}\n"
                    f"時間:{alert.created_at}"
                )
            }
        ]
    }

    response = requests.post(
        url,
        headers=headers,
        json=body,
        timeout=15
    )

    response.raise_for_status()
Channel Access Token、User ID 與伺服器金鑰不可直接寫入公開的程式庫或部落格,應放入環境變數。

八、使用 Firebase Cloud Messaging

若系統已有 Android、iOS 或 Web App,可透過 Firebase Cloud Messaging(FCM)發送通知。正式環境建議由可信任的伺服器端使用 Firebase Admin SDK。

Python/Firebase Admin SDK 概念
import firebase_admin
from firebase_admin import credentials
from firebase_admin import messaging

def send_fcm_alert(alert, device_token):
    message = messaging.Message(
        notification=messaging.Notification(
            title="智慧養殖警示",
            body=alert.message
        ),
        data={
            "pond_id": str(alert.pond_id),
            "alert_id": str(alert.id),
            "level": alert.level
        },
        token=device_token
    )

    return messaging.send(message)

九、統一派送通知

dispatch_alert()
def dispatch_alert(alert):
    errors = []

    try:
        send_alert_email(alert)
    except Exception as exc:
        errors.append(
            f"Email:{exc}"
        )

    try:
        send_line_message(
            alert,
            alert.pond.manager_line_user_id
        )
    except Exception as exc:
        errors.append(
            f"LINE:{exc}"
        )

    try:
        send_fcm_alert(
            alert,
            alert.pond.manager_device_token
        )
    except Exception as exc:
        errors.append(
            f"FCM:{exc}"
        )

    return errors
正式系統不宜讓 API Request 等待三種通知全部完成。可將通知工作交給 Celery、RQ 或背景工作佇列執行。

十、避免重複警示洗版

若 ESP32 每 15 秒上傳一次,而 DO 持續偏低,系統可能每 15 秒傳一次訊息。應設定冷卻時間:

冷卻時間範例
from django.utils import timezone
from datetime import timedelta

def recently_sent(
    pond,
    alert_type,
    minutes=10
):
    since = (
        timezone.now()
        - timedelta(minutes=minutes)
    )

    return Alert.objects.filter(
        pond=pond,
        alert_type=alert_type,
        created_at__gte=since,
        is_resolved=False
    ).exists()

建立警示前先檢查:

避免重複建立
if not recently_sent(
    reading.pond,
    item["type"],
    minutes=10
):
    Alert.objects.create(...)

十一、加入恢復通知

只通知異常還不夠。數值恢復正常時,也應通知管理者:

恢復概念
active_alerts = Alert.objects.filter(
    pond=reading.pond,
    alert_type="LOW_DO",
    is_resolved=False
)

if (
    reading.dissolved_oxygen is not None
    and reading.dissolved_oxygen >= 4.5
):
    for alert in active_alerts:
        alert.is_resolved = True
        alert.resolved_at = timezone.now()
        alert.save()

        send_recovery_notification(alert)
建議使用「觸發值」與「恢復值」兩個門檻,例如 DO 低於 4.0 觸發、回升到 4.5 才解除,可避免數值在邊界附近反覆跳動。

十二、Dashboard 顯示警示紀錄

時間魚塭類型訊息等級狀態
14:32:151 號池溶氧異常DO:3.20 mg/LCRITICAL未處理
14:31:401 號池水溫異常水溫:33.5°CWARNING未處理
14:20:102 號池pH 異常pH:9.10WARNING已讀
Template 狀態顏色
{% if alert.level == "CRITICAL" %}
  <span class="badge bg-danger">
    CRITICAL
  </span>

{% elif alert.level == "WARNING" %}
  <span class="badge bg-warning text-dark">
    WARNING
  </span>

{% else %}
  <span class="badge bg-info">
    INFO
  </span>
{% endif %}

十三、警示狀態處理

警示頁應提供:

  • 標記為已讀。
  • 標記為已處理。
  • 加入處理說明。
  • 顯示解除時間。
  • 依魚塭、等級、日期篩選。
  • 匯出 CSV。

十四、資料逾時也需要警示

即使水質數值正常,若設備長時間沒有上傳,也可能代表斷電、Wi-Fi 中斷或感測器故障。

資料逾時判斷
from django.utils import timezone
from datetime import timedelta

last_reading = (
    pond.readings
    .order_by("-recorded_at")
    .first()
)

if (
    last_reading is None
    or last_reading.recorded_at
       < timezone.now()
         - timedelta(minutes=5)
):
    create_offline_alert(pond)

十五、建議的警示分級

等級用途通知方式
INFO設備恢復、校正完成Dashboard 紀錄
WARNING水溫或 pH 接近危險值Email 或 LINE
CRITICAL溶氧過低、設備離線Email+LINE+App 推播

十六、常見問題與排除

問題可能原因處理方式
收不到 EmailSMTP、Port、應用程式密碼錯誤檢查 Django Email 設定與垃圾郵件
LINE 推播失敗Token、User ID、好友關係或權限問題查看 Messaging API 回應內容
FCM 收不到裝置 Token 過期或 App 權限未開更新 Token 並檢查通知權限
同一異常重複通知沒有冷卻時間加入 recently_sent()
異常解除後仍顯示沒有恢復判斷加入 is_resolved 與 resolved_at
API 變慢同步發送多個通知改用背景工作佇列

十七、完整整合流程

Atlas EZO ESP32 Django API 儲存資料 判斷異常 建立 Alert 多管道通知

十八、本篇重點

  1. 異常判斷集中在 Django,方便統一管理。
  2. 每次異常都應建立可追蹤的 Alert 紀錄。
  3. Email、LINE Messaging API 與 FCM 可依等級搭配。
  4. 必須設定冷卻時間,避免重複洗版。
  5. 恢復正常時也應發送解除通知。
  6. 資料逾時與設備離線同樣需要警示。
  7. 正式系統宜使用背景工作處理通知。

結語

完成 EP07 後,智慧養殖平台不再只是被動顯示數據,而能主動發現問題、保存異常紀錄並通知管理者。當警示系統加入分級、冷卻時間、恢復判斷與多管道派送後,才能避免訊息洗版,同時讓真正重要的異常被快速看見。這是智慧養殖系統從「監控」走向「主動管理」的重要一步。

技術更新說明:LINE 通知部分採用 LINE Messaging API;App 推播採用 Firebase Cloud Messaging。部署前請依官方文件建立權杖、設定接收對象與檢查服務限制。
AI 協作聲明:本文由作者主導智慧養殖警示架構、異常門檻概念、Django 資料模型、通知流程與場域需求設計,並使用生成式 AI 協助文字整理、程式碼說明、版面配置與教學資訊圖生成;文章中的範例門檻僅供教學,正式養殖警示值須由作者與場域專業人員依實際需求確認。

沒有留言:

張貼留言