# MikroTik PPPoE Monitor - Telegram Notification

## Cài nhanh bằng Terminal

### Chuẩn bị trên RouterOS 7

RouterOS 7 có thể chặn `fetch` hoặc Scheduler bằng `device-mode`. Kiểm tra trước khi cài:

```routeros
/system/device-mode/print
```

Nếu thấy `fetch: no` hoặc `scheduler: no`, chỉ bật hai tính năng cần thiết:

```routeros
/system/device-mode/update fetch=yes scheduler=yes
```

Trong thời gian router hiển thị, thường là 5 phút, **rút nguồn rồi cắm lại** để xác nhận vật lý. Không dùng `/system reboot` thay cho bước này. Sau khi router khởi động xong, kiểm tra lại:

```routeros
/system/device-mode/print
```

Chỉ tiếp tục khi kết quả có đủ:

```text
fetch: yes
scheduler: yes
flagged: no
```

Nếu gặp `failure: not allowed by device-mode`, chưa chạy lại installer ngay; hãy bật `fetch` và `scheduler` theo các bước trên. Nếu `flagged: yes`, dừng cài đặt và kiểm tra an toàn của router trước khi gỡ trạng thái cảnh báo. Installer không thể tự bật các tính năng này vì chính `fetch` đang bị RouterOS chặn.

Tài liệu chính thức: [MikroTik RouterOS Device-mode](https://help.mikrotik.com/docs/spaces/ROS/pages/93749258/Device-mode).

### Chạy installer

Mặc định installer dùng `pppoe-out1` và tự thêm `pppoe-out2` nếu interface này tồn tại. Dán từng bước ASCII sau vào Terminal:

1. Nhập Bot Token và Chat ID:

```routeros
:global pppMonInstallBotToken "BOT_TOKEN"; :global pppMonInstallChatId "CHAT_ID"
```

2. Nếu interface không dùng tên mặc định, khai báo riêng trước lệnh `fetch`:

```routeros
:global pppMonInstallPppoe1 "WAN1"; :global pppMonInstallPppoe2 "WAN2";
```

Nếu dùng đúng `pppoe-out1` và `pppoe-out2`, bỏ qua bước này. Cuối cùng tải và chạy installer:

```routeros
/tool fetch url="https://mikrotik.thachanhitt.vn/pppoe-monitor-install.rsc" dst-path="pppoe-monitor-install.rsc" check-certificate=yes; /import file-name="pppoe-monitor-install.rsc"; /file remove [find where name="pppoe-monitor-install.rsc"]
```

Installer tự tạo hoặc cập nhật route probe, `pppoe-monitor-config`, `pppoe-monitor`, `pppoe-monitor-reset` và Scheduler `pppoe-monitor-every-3m`. Bot Token và Chat ID chỉ được lưu trong script cấu hình trên router, không nằm trong URL công khai.

Sau khi cài, chạy thử và xem log:

```routeros
/system script run pppoe-monitor
/log print where message~"PPPoE-MON"
```

Phải thấy `INSTALL | SUCCESS` và `DONE`. Việc kiểm tra thực tế trên RouterOS 6.49.x hoặc 7.x vẫn bắt buộc trước khi dùng chính thức.

## Cảnh báo mạng mở rộng (tùy chọn)

Phần này đã tách riêng: [Cài đặt cảnh báo mạng mở rộng](canh-bao-mang-mo-rong.md). File đó có lệnh cài nhanh, lệnh bật/tắt từng nhóm và hướng dẫn tạo thủ công trong WinBox/WebFig.

## Cài thủ công

1. Đặt tên nhà trong **System → Identity**. Nếu dùng CLI, chỉ đặt tên ASCII:

   ```routeros
   /system identity set name="NHA-ANH-THACH"
   ```

2. Tạo route probe:

   ```routeros
   /ip route
   add dst-address=8.8.8.8/32 gateway=pppoe-out1 comment="PPPoE-monitor-isp1"
   add dst-address=9.9.9.9/32 gateway=pppoe-out2 comment="PPPoE-monitor-isp2"
   add dst-address=208.67.220.220/32 gateway=pppoe-out3 comment="PPPoE-monitor-isp3"
   add dst-address=94.140.14.14/32 gateway=pppoe-out4 comment="PPPoE-monitor-isp4"
   ```

   Chỉ thêm route cho WAN đang sử dụng. Nếu đã có route cùng comment, chỉnh route đó, không thêm trùng.

3. Vào **System → Scripts → +**:

   - Name: `pppoe-monitor-config`
   - Policy: `read`, `policy`, `test`
   - Source: dán khối dưới và sửa thông tin thật. WAN nào không sử dụng thì giữ `pppMonPppoe2`/`3`/`4` là chuỗi rỗng; WAN có sử dụng phải điền đúng tên interface tương ứng với route.

   ```routeros
   :global pppMonBotToken "BOT_TOKEN"
   :global pppMonChatId "CHAT_ID"
   :global pppMonNotifyChannel "telegram"
   :global pppMonMaxLines 4
   :global pppMonFailLimit 3
   :global pppMonPingCount 3
   :global pppMonRecoveryHoldTime 5m
   :global pppMonDebug true
   :global pppMonPppoe1 "pppoe-out1"
   :global pppMonPppoe2 ""
   :global pppMonPppoe3 ""
   :global pppMonPppoe4 ""
   :global pppMonProbe1 "8.8.8.8"
   :global pppMonProbe2 "9.9.9.9"
   :global pppMonProbe3 "208.67.220.220"
   :global pppMonProbe4 "94.140.14.14"
   ```

4. Tạo Script thứ hai:

   - Name: `pppoe-monitor`
   - Policy: `read`, `write`, `policy`, `test`
   - Source: dán khối bên dưới, không cần nhập lại Bot Token.

> Source được dán vào giao diện Scripts, không dán vào Terminal. Trong CLI chỉ dùng ASCII, không dùng tiếng Việt có dấu.
> Đổi tên WAN trong `pppMonPppoe1` tới `pppMonPppoe4` của config và `gateway=` ở route tương ứng. Không sửa hàm `$checkLine`; nguồn dưới được đồng bộ từ installer.

### SOURCE

```routeros
    /system script run pppoe-monitor-config

    :global pppMonBotToken
    :global pppMonChatId
    :global pppMonFailLimit
    :global pppMonPingCount
    :global pppMonRecoveryHoldTime
    :global pppMonDebug
    :global pppMonPppoe1
    :global pppMonPppoe2
    :global pppMonPppoe3
    :global pppMonPppoe4
    :global pppMonProbe1
    :global pppMonProbe2
    :global pppMonProbe3
    :global pppMonProbe4

    :global pppMonFailCount
    :global pppMonLastAlert
    :global pppMonAlertOpen
    :global pppMonOutageActive
    :global pppMonOutageStartedAt
    :global pppMonOutageStartUptime
    :global pppMonRecoveryHoldUntil

    :if ([:typeof $pppMonFailCount] != "array") do={
        :set pppMonFailCount {"_init"=0}
    }
    :if ([:typeof $pppMonLastAlert] != "array") do={
        :set pppMonLastAlert {"_init"=""}
    }
    :if ([:typeof $pppMonAlertOpen] != "array") do={
        :set pppMonAlertOpen {"_init"=false}
    }
    :if ([:typeof $pppMonOutageActive] != "array") do={
        :set pppMonOutageActive {"_init"=false}
    }
    :if ([:typeof $pppMonOutageStartedAt] != "array") do={
        :set pppMonOutageStartedAt {"_init"=""}
    }
    :if ([:typeof $pppMonOutageStartUptime] != "array") do={
        :set pppMonOutageStartUptime {"_init"=0}
    }
    :if ([:typeof $pppMonRecoveryHoldUntil] != "array") do={
        :set pppMonRecoveryHoldUntil {"_init"=0}
    }

    :if ($pppMonDebug = true) do={
        :log info ("[PPPoE-MON] RUN | router=" . [/system identity get name])
    }

    :local checkLine do={
        :global pppMonBotToken
        :global pppMonChatId
        :global pppMonFailLimit
        :global pppMonPingCount
        :global pppMonRecoveryHoldTime
        :global pppMonDebug
        :global pppMonFailCount
        :global pppMonLastAlert
        :global pppMonAlertOpen
        :global pppMonOutageActive
        :global pppMonOutageStartedAt
        :global pppMonOutageStartUptime
        :global pppMonRecoveryHoldUntil

        :local notify do={
            :global pppMonBotToken
            :global pppMonChatId
            :global pppMonDebug

            :local pppoeId [/interface pppoe-client find where name=$pppoeName]
            :if ([:len $pppoeId] = 0) do={
                :log error ("[PPPoE-MON] ERROR | PPPoE not found | interface=" . $pppoeName)
                :return false
            }

            :local routerName [/system identity get name]
            :local pppoeUser [/interface pppoe-client get $pppoeId user]
            :local wanPort [/interface pppoe-client get $pppoeId interface]
            :local pppoeComment [/interface pppoe-client get $pppoeId comment]
            :local currentIp "Không có"

            :if ([:len $pppoeUser] = 0) do={ :set pppoeUser "Không khai báo" }
            :if ([:len $pppoeComment] = 0) do={ :set pppoeComment "Không có" }

            :do {
                :local addressIds [/ip address find where interface=$pppoeName]
                :if ([:len $addressIds] > 0) do={
                    :set currentIp [/ip address get [:pick $addressIds 0] address]
                }
            } on-error={ :set currentIp "Không có" }

            :local tgHeader ""
            :if ($eventName = "down") do={
                :set tgHeader "\F0\9F\94\B4 MẤT KẾT NỐI INTERNET"
            } else={
                :if ($eventName = "recovered-late") do={
                    :set tgHeader "\F0\9F\9F\A2 INTERNET ĐÃ KHÔI PHỤC SAU SỰ CỐ"
                } else={
                    :set tgHeader "\F0\9F\9F\A2 INTERNET ĐÃ KHÔI PHỤC"
                }
            }

            :local tgText ($tgHeader . "\n")
            :set tgText ($tgText . "------------------------------\n")
            :set tgText ($tgText . "\F0\9F\8F\A0 Nhà / Router: " . $routerName . "\n")
            :set tgText ($tgText . "\F0\9F\8C\90 Đường truyền: " . $lineLabel . "\n")
            :set tgText ($tgText . "\F0\9F\91\A4 Tài khoản PPPoE: " . $pppoeUser . "\n")
            :set tgText ($tgText . "\F0\9F\94\8C Interface: " . $pppoeName . " qua " . $wanPort . "\n")
            :set tgText ($tgText . "\F0\9F\93\9D Comment: " . $pppoeComment . "\n")
            :set tgText ($tgText . "\F0\9F\8C\8D IP hiện tại: " . $currentIp . "\n")
            :set tgText ($tgText . "\F0\9F\93\A1 Điểm kiểm tra: " . $probeAddress . "\n")
            :if ($eventName != "down") do={
                :set tgText ($tgText . "Bắt đầu mất: " . $outageStartedAt . "\n")
                :set tgText ($tgText . "Thời gian gián đoạn: " . $outageDuration . "\n")
                :if ($eventName = "recovered-late") do={
                    :set tgText ($tgText . \
                        "Ghi chú: Không thể gửi lúc mất mạng; đây là báo bù.\n")
                }
            }
            :set tgText ($tgText . "\F0\9F\95\92 Thời gian: " . [/system clock get date] . " " . [/system clock get time] . "\n")

            :local formText ""
            :for formIndex from=0 to=([:len $tgText] - 1) do={
                :local formChar [:pick $tgText $formIndex ($formIndex + 1)]
                :if ($formChar = "%") do={
                    :set formText ($formText . "%25")
                } else={
                    :if ($formChar = "&") do={
                        :set formText ($formText . "%26")
                    } else={
                        :if ($formChar = "+") do={
                            :set formText ($formText . "%2B")
                        } else={
                            :if ($formChar = "=") do={
                                :set formText ($formText . "%3D")
                            } else={
                                :if ($formChar = " ") do={
                                    :set formText ($formText . "%20")
                                } else={
                                    :if ($formChar = "\n") do={
                                        :set formText ($formText . "%0A")
                                    } else={
                                        :if ($formChar = "\r") do={
                                            :set formText $formText
                                        } else={
                                            :set formText ($formText . $formChar)
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            :local telegramUrl ("https://api.telegram.org/bot" . $pppMonBotToken . "/sendMessage")
            :local requestBody ("chat_id=" . $pppMonChatId . "&text=" . $formText)
            :local telegramSent false

            :if ($pppMonDebug = true) do={
                :log info ("[PPPoE-MON] TELEGRAM | event=" . $eventName . \
                    " | line=" . $lineLabel . " | status=SENDING")
            }

            :do {
                /tool fetch url=$telegramUrl http-method=post \
                    http-header-field="Content-Type:application/x-www-form-urlencoded" \
                    http-data=$requestBody keep-result=no check-certificate=yes
                :set telegramSent true
                :log info ("[PPPoE-MON] TELEGRAM | event=" . $eventName . \
                    " | line=" . $lineLabel . " | status=SUCCESS")
            } on-error={
                :log error ("[PPPoE-MON] TELEGRAM | event=" . $eventName . \
                    " | line=" . $lineLabel . " | status=FAILED")
            }

            :return $telegramSent
        }

        :local pppoeId [/interface pppoe-client find where name=$pppoeName]
        :if ([:len $pppoeId] = 0) do={
            :log error ("[PPPoE-MON] ERROR | check skipped | interface=" . $pppoeName)
            :return
        }

        :local failStateType [:typeof ($pppMonFailCount->$lineKey)]
        :if (($failStateType = "nothing") || ($failStateType = "nil")) do={
            :set ($pppMonFailCount->$lineKey) 0
        }
        :local alertDateStateType [:typeof ($pppMonLastAlert->$lineKey)]
        :if (($alertDateStateType = "nothing") || ($alertDateStateType = "nil")) do={
            :set ($pppMonLastAlert->$lineKey) ""
        }
        :local alertOpenStateType [:typeof ($pppMonAlertOpen->$lineKey)]
        :if (($alertOpenStateType = "nothing") || ($alertOpenStateType = "nil")) do={
            :set ($pppMonAlertOpen->$lineKey) false
        }
        :local outageStateType [:typeof ($pppMonOutageActive->$lineKey)]
        :if (($outageStateType = "nothing") || ($outageStateType = "nil")) do={
            # Preserve an open alert when upgrading from an older installer.
            :set ($pppMonOutageActive->$lineKey) ($pppMonAlertOpen->$lineKey)
        }
        :local startedAtType [:typeof ($pppMonOutageStartedAt->$lineKey)]
        :if (($startedAtType = "nothing") || ($startedAtType = "nil")) do={
            :set ($pppMonOutageStartedAt->$lineKey) "Không xác định"
        }
        :local startUptimeType [:typeof ($pppMonOutageStartUptime->$lineKey)]
        :if (($startUptimeType = "nothing") || ($startUptimeType = "nil")) do={
            :set ($pppMonOutageStartUptime->$lineKey) 0
        }
        :local holdUntilType [:typeof ($pppMonRecoveryHoldUntil->$lineKey)]
        :if (($holdUntilType = "nothing") || ($holdUntilType = "nil")) do={
            :set ($pppMonRecoveryHoldUntil->$lineKey) 0
        }

        :if ($pppMonDebug = true) do={
            :log info ("[PPPoE-MON] CHECK | line=" . $lineLabel . \
                " | interface=" . $pppoeName . " | probe=" . $probeAddress)
        }

        :local pingReplies 0
        :do {
            :set pingReplies [/ping address=$probeAddress interface=$pppoeName \
                count=$pppMonPingCount interval=500ms]
        } on-error={ :set pingReplies 0 }

        :if ($pingReplies > 0) do={
            :if ($pppMonDebug = true) do={
                :log info ("[PPPoE-MON] RESULT | line=" . $lineLabel . \
                    " | status=UP | replies=" . $pingReplies . "/" . $pppMonPingCount . \
                    " | previous-fails=" . ($pppMonFailCount->$lineKey))
            }

            :if (($pppMonOutageActive->$lineKey) = true) do={
                :local recoveryEvent "recovered-late"
                :if (($pppMonAlertOpen->$lineKey) = true) do={
                    :set recoveryEvent "up"
                }
                :local outageDuration "Không xác định"
                :local savedUptime ($pppMonOutageStartUptime->$lineKey)
                :if ([:typeof $savedUptime] = "time") do={
                    :set outageDuration \
                        ([/system resource get uptime] - $savedUptime)
                }
                :local recoverySent [$notify eventName=$recoveryEvent \
                    lineLabel=$lineLabel pppoeName=$pppoeName \
                    probeAddress=$probeAddress \
                    outageStartedAt=($pppMonOutageStartedAt->$lineKey) \
                    outageDuration=$outageDuration]
                :if ($recoverySent = true) do={
                    :set ($pppMonAlertOpen->$lineKey) false
                    :set ($pppMonOutageActive->$lineKey) false
                    :set ($pppMonOutageStartedAt->$lineKey) ""
                    :set ($pppMonOutageStartUptime->$lineKey) 0
                    :set ($pppMonRecoveryHoldUntil->$lineKey) \
                        ([/system resource get uptime] + $pppMonRecoveryHoldTime)
                }
            } else={
                :set ($pppMonOutageStartedAt->$lineKey) ""
                :set ($pppMonOutageStartUptime->$lineKey) 0
            }

            :set ($pppMonFailCount->$lineKey) 0
        } else={
            :local failCount (($pppMonFailCount->$lineKey) + 1)
            :if ($failCount > $pppMonFailLimit) do={ :set failCount $pppMonFailLimit }
            :set ($pppMonFailCount->$lineKey) $failCount
            :if (($failCount = 1) && \
                    (($pppMonOutageActive->$lineKey) = false)) do={
                :set ($pppMonOutageStartedAt->$lineKey) \
                    ([/system clock get date] . " " . [/system clock get time])
                :set ($pppMonOutageStartUptime->$lineKey) \
                    [/system resource get uptime]
            }
            :log warning ("[PPPoE-MON] RESULT | line=" . $lineLabel . \
                " | status=DOWN | replies=0/" . $pppMonPingCount . \
                " | fail=" . $failCount . "/" . $pppMonFailLimit)

            :local today [/system clock get date]
            :if ($failCount >= $pppMonFailLimit) do={
                :set ($pppMonOutageActive->$lineKey) true
                :if ((($pppMonAlertOpen->$lineKey) = false) && \
                        (($pppMonLastAlert->$lineKey) != $today)) do={
                    :local alertSent [$notify eventName="down" lineLabel=$lineLabel \
                        pppoeName=$pppoeName probeAddress=$probeAddress]
                    :if ($alertSent = true) do={
                        :set ($pppMonLastAlert->$lineKey) $today
                        :set ($pppMonAlertOpen->$lineKey) true
                    }
                } else={
                    :if ($pppMonDebug = true) do={
                        :log info ("[PPPoE-MON] ALERT-SKIP | line=" . $lineLabel . \
                            " | reason=already-sent-today")
                    }
                }
            }
        }

        :if ($pppMonDebug = true) do={
            :log info ("[PPPoE-MON] END | line=" . $lineLabel)
        }
    }

    $checkLine lineKey="isp1" lineLabel="ISP 1 - đường chính" \
        pppoeName=$pppMonPppoe1 probeAddress=$pppMonProbe1

    :if ([:len $pppMonPppoe2] > 0) do={
        $checkLine lineKey="isp2" lineLabel="ISP 2 - dự phòng" \
            pppoeName=$pppMonPppoe2 probeAddress=$pppMonProbe2
    }
    :if ([:len $pppMonPppoe3] > 0) do={
        $checkLine lineKey="isp3" lineLabel="ISP 3" \
            pppoeName=$pppMonPppoe3 probeAddress=$pppMonProbe3
    }
    :if ([:len $pppMonPppoe4] > 0) do={
        $checkLine lineKey="isp4" lineLabel="ISP 4" \
            pppoeName=$pppMonPppoe4 probeAddress=$pppMonProbe4
    }

    :if ($pppMonDebug = true) do={
        :log info "[PPPoE-MON] DONE | all configured lines checked"
    }
```

## Scheduler

Vào **System → Scheduler → +**:

- Name: `pppoe-monitor-every-3m`
- Interval: `00:03:00`
- On Event: `/system script run pppoe-monitor`
- Policy: `read`, `write`, `policy`, `test`

CLI ASCII tương đương:

```routeros
/system scheduler add name="pppoe-monitor-every-3m" interval=3m on-event="/system script run pppoe-monitor" policy=read,write,policy,test comment="PPPoE-monitor"
```

## Thêm WAN

Nguồn hiện hỗ trợ tối đa 4 WAN. Điền `pppMonPppoe3`/`pppMonPppoe4` trong config và thêm route probe WAN tương ứng ở trên. Không tự thêm lời gọi `$checkLine`, không dùng probe trùng với generator/WARP. Xem [khai báo và nâng cấp 4 WAN](cai-dat-canh-bao.md).

## Kiểm tra

- Chạy thử: **System → Scripts → pppoe-monitor → Run Script**.
- Xem log: **Log**, tìm `PPPoE-MON`; phải có `DONE` sau khi kiểm tra hết WAN.
- Giảm log thường kỳ: đổi `:global pppMonDebug true` thành `false` trong Source.

## Reset để test lại

Tạo script thứ hai trong **System → Scripts** với Name: `pppoe-monitor-reset`. Dán Source sau, rồi nhấn **Run Script** trước khi test. Tạo bằng cùng tài khoản đang sở hữu `pppoe-monitor`.

```routeros
:global pppMonFailCount
:global pppMonLastAlert
:global pppMonAlertOpen
:global pppMonOutageActive
:global pppMonOutageStartedAt
:global pppMonOutageStartUptime
:global pppMonRecoveryHoldUntil

:set pppMonFailCount {"_init"=0}
:set pppMonLastAlert {"_init"=""}
:set pppMonAlertOpen {"_init"=false}
:set pppMonOutageActive {"_init"=false}
:set pppMonOutageStartedAt {"_init"=""}
:set pppMonOutageStartUptime {"_init"=0}
:set pppMonRecoveryHoldUntil {"_init"=0}

:log warning "[PPPoE-MON] STATE | reset"
```

CLI ASCII để xem log:

```routeros
/log print where message~"PPPoE-MON"
```

Không đưa Bot Token vào GitHub hoặc chat công khai.
