前一篇已完成 Django Dashboard,即時呈現水溫、pH、溶氧與鹽度。EP07 將進一步建立「主動警示」:當水質超出設定範圍,Django 自動建立警示紀錄,並透過 Email、LINE Messaging API 或 Firebase Cloud Messaging 將訊息送到管理者手機。
版本提醒:資訊圖中的「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 |
| pH | 6.0~8.5 | 低於 6.0 或高於 8.5 |
| 溶氧 DO | 高於 4.0 mg/L | 低於 4.0 mg/L |
| 導電度 EC | 200~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:15 | 1 號池 | 溶氧異常 | DO:3.20 mg/L | CRITICAL | 未處理 |
| 14:31:40 | 1 號池 | 水溫異常 | 水溫:33.5°C | WARNING | 未處理 |
| 14:20:10 | 2 號池 | pH 異常 | pH:9.10 | WARNING | 已讀 |
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 推播 |
十六、常見問題與排除
| 問題 | 可能原因 | 處理方式 |
|---|---|---|
| 收不到 Email | SMTP、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
→
多管道通知
十八、本篇重點
- 異常判斷集中在 Django,方便統一管理。
- 每次異常都應建立可追蹤的 Alert 紀錄。
- Email、LINE Messaging API 與 FCM 可依等級搭配。
- 必須設定冷卻時間,避免重複洗版。
- 恢復正常時也應發送解除通知。
- 資料逾時與設備離線同樣需要警示。
- 正式系統宜使用背景工作處理通知。
結語
完成 EP07 後,智慧養殖平台不再只是被動顯示數據,而能主動發現問題、保存異常紀錄並通知管理者。當警示系統加入分級、冷卻時間、恢復判斷與多管道派送後,才能避免訊息洗版,同時讓真正重要的異常被快速看見。這是智慧養殖系統從「監控」走向「主動管理」的重要一步。
技術更新說明:LINE 通知部分採用 LINE Messaging API;App 推播採用 Firebase Cloud Messaging。部署前請依官方文件建立權杖、設定接收對象與檢查服務限制。
AI 協作聲明:本文由作者主導智慧養殖警示架構、異常門檻概念、Django 資料模型、通知流程與場域需求設計,並使用生成式 AI 協助文字整理、程式碼說明、版面配置與教學資訊圖生成;文章中的範例門檻僅供教學,正式養殖警示值須由作者與場域專業人員依實際需求確認。

沒有留言:
張貼留言