Note on the daily report. The cron task routes via Dispatcharr's
dvrCelery worker as a workaround for an upstream plugin-task-registration issue affecting the default prefork worker pool (Dispatcharr#1244). The routing is transparent — no user action required. Event-driven alerts (channel_start,stream_switch,vod_start, etc.) use a different code path and have no Celery dependency.
A Dispatcharr plugin that pushes channel, stream, and VOD events to a Telegram chat via a bot — plus an optional cron-driven daily report covering public IP, geo, bandwidth, activity stats, and source health.
- Manual test action — verify your bot before trusting it with real alerts.
- Event-driven — subscribes to Dispatcharr's
channel_start,channel_stop,channel_reconnect,stream_switch,vod_start, andvod_stopevents. Per-event toggles let you control noise. - Daily report (optional) — public IP + geo, Cloudflare speedtest, activity stats since last report, and M3U/EPG source health. Cron-scheduled via Celery beat.
- HTML formatting with safe escaping; a plain-text fallback is also available.
- Optional enrichment — opt-in toggles to include the channel/VOD's M3U source and the EPG "now playing" title.
- Zero external dependencies — uses only the Python standard library.
- Per-instance label — tag every message with which Dispatcharr instance it came from.
Dispatcharr → Plugins → Find Plugins → search "Telegram Alerts" → Install. Updates also surface here. Listed in the official catalogue since v0.4.5.
- Download
plugin-telegram-alerts-v<version>.zipfrom a GitHub release. - Dispatcharr → Plugins → Import → upload the zip.
- Enable Telegram Alerts in the Plugins tab.
You'll need a bot token and a chat ID.
- Open Telegram and message @BotFather.
- Send
/newbotand follow the prompts (give it a name and a unique_botusername). - BotFather will reply with a token of the form
123456789:ABCdefGhIJklmNOpqrsTUVwxyz. - Copy this token — you'll paste it into the Plugin's Bot Token setting.
You can send the alerts to either a personal DM with the bot or a group/channel.
For a personal DM:
- Open Telegram, search for your new bot, and send it any message (e.g.
hello). - In your browser, visit:
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates - Look for
"chat":{"id": 123456789, ...}in the JSON. That number is your chat ID.
For a group:
- Add the bot to your group.
- Send any message in the group (the bot needs at least one update to appear).
- Hit
/getUpdatesas above. - The chat ID will be a negative number (e.g.
-1001234567890for supergroups).
If
/getUpdatesreturns an empty array, send another message and try again — Telegram only returns recent updates.
- Open Dispatcharr → Plugins → Telegram Alerts → Settings.
- Paste the Bot Token and Chat ID.
- Set the Instance Label (optional but useful — e.g.
YodaorBackup-NAS). - Toggle which events you care about under [ALERTS] Toggles. Defaults: only
channel_reconnectis on. - Click Save.
- Switch to the Actions tab and click Send Test. You should see a message arrive in your chat within a second.
Each alert is a short structured message. Example for a channel event (HTML mode, both enrichment toggles on):
🔄 [Yoda] Channel reconnected
Channel: ESPN
Stream: backup-feed
Source: MyIPTVProvider
Now playing: NFL Live
Example for a VOD event (HTML mode, Include Stream Source on):
🎬 [Yoda] VOD started
Title: Inception
Source: MyIPTVProvider
Field meanings:
| Field | Source |
|---|---|
| Emoji + label | Per-event severity marker (▶ start, ⏹ stop, 🔄 reconnect, 🔀 switch, 🎬 VOD start, 🛑 VOD stop, ✅ test) |
[Yoda] |
The Instance Label setting |
| Channel | payload.channel_name (channel events only) |
| Title | payload.content_name (VOD events only — movie or episode title) |
| Stream | payload.stream_name (only present for stream_switch) |
| Source | M3U account name backing the channel or VOD — only when Include Stream Source is on |
| Now playing | EPG title where start_time ≤ now < end_time — only when Include Current EPG Program is on, channels only (VODs have no EPG) |
Optional fields are skipped silently when the lookup returns nothing (e.g. no EPG mapping), so messages stay tidy.
Channel UUIDs are not included — Dispatcharr's event payload doesn't carry them. VOD events do carry a content_uuid, used internally to look up the VOD's M3U source.
| Setting | Type | Default | Notes |
|---|---|---|---|
| Bot Token | string | — | From @BotFather. Masked in logs. |
| Chat ID | string | — | Numeric. Group IDs are negative. |
| Instance Label | string | Dispatcharr |
Prefixed in every message. |
| Alert on Channel Start | boolean | off | Noisy if you have many channels. |
| Alert on Channel Stop | boolean | off | Noisy. |
| Alert on Channel Reconnect | boolean | on | Useful warning signal. |
| Alert on Stream Switch | boolean | off | Fires whenever a stream URL is swapped. |
| Alert on VOD Start | boolean | off | Fires every time a movie/episode starts playing. Chatty in multi-user setups. |
| Alert on VOD Stop | boolean | off | Fires every time VOD playback ends. |
| Include Stream Source | boolean | off | Adds the M3U account name to each alert. Channels look up via priority order; VODs look up via the Movie/Episode/Series M3U relation. One DB lookup per event. |
| Include Current EPG Program | boolean | off | Adds the currently-airing program title. Requires the channel to have an EPG mapping. One DB lookup per event. |
| Message Format | select | HTML |
HTML or plain. |
| Enable Daily Report | boolean | off | Master toggle for the cron-driven daily report. After turning on, click Apply Schedule in the Actions tab to register the cron. |
| Report Schedule (cron) | string | 0 9 * * * |
5-field cron. Default = every day 09:00 in the timezone below. |
| Report Timezone | string | (empty = UTC) | IANA name (e.g. Europe/London, Australia/Brisbane). Re-Apply after changing. |
| Report Chat ID | string | (empty) | Send report to a different chat than per-event alerts. Blank = use main Chat ID. |
| Include Network Section | boolean | on | Public IP + geographic location lookup. |
| Include Speedtest | boolean | on | Down/up bandwidth via Cloudflare. ~150 MB per test, respects the cooldown below. |
| Speedtest Cooldown (hours) | number | 6 |
Minimum hours between speedtests. Lets you run reports hourly without burning bandwidth. |
| Include Activity Section | boolean | on | Channel plays, top channels, VOD plays, errors, stream switches since the previous report. |
| Include Sources Section | boolean | on | M3U account count + EPG source freshness. |
| Action | Description |
|---|---|
| Send Test | Posts a one-off "Telegram Alerts test" message. Validates token, chat ID, and formatting end-to-end. |
| Send Report Now | Build and send a daily report immediately. Window = since the previous report. Advances the "last report" marker on successful send. |
| Apply Schedule | Register or update the cron task in django-celery-beat. Re-click after changing any report setting (snapshots fresh settings into the task). |
| Show Schedule Status | Show the registered cron, last run, total runs. |
| Remove Schedule | Unregister the periodic task. |
| Handle channel/stream/VOD event (internal) | Dispatcharr fires this automatically — you should never click it. Hidden in normal use. |
Test message fails with Telegram HTTP 401
Token is wrong. Re-copy it from @BotFather.
Test message fails with Telegram HTTP 400: chat not found
The bot has never received a message from that chat. DM the bot once (or post in the group) so Telegram knows the chat exists, then retry.
Test message succeeds but event alerts never arrive
- Check the relevant per-event toggle is on.
- Confirm Dispatcharr is actually emitting the event (try starting a channel).
- Look in the Dispatcharr logs for lines starting with
on_event[<event>].
Unknown action: <id> after a code change
Dispatcharr workers cache the plugin module. Restart the Dispatcharr container — touching .reload_token is not enough.
Chat ID validation rejects my ID
The plugin requires a numeric integer, optionally with a leading - for groups. Strip any whitespace or quotes.
git clone https://github.com/R3XCHRIS/telegram-alerts
cd telegram-alerts
pip install pytest
python3 -m pytest tests/The tests cover the pure helpers (token masking, HTML escaping, message formatting, credential validation). Network-dependent code in _send_telegram is exercised by the Send Test action against the live Telegram API.
Off by default. To enable:
- Tick Enable Daily Report in Settings, pick your cron in Report Schedule (default
0 9 * * *= every day at 09:00). - Optionally toggle off whichever sections you don't want.
- Save.
- Switch to the Actions tab, click Apply Schedule — that registers (or updates) a
django-celery-beatperiodic task. - Click Send Report Now once to verify the format end-to-end.
The activity stats window is "since the previous report", not a fixed 24 hours. So:
- Daily cron → each report covers the last day.
- Weekly cron → each report covers the last week.
- Hourly cron → each report covers the last hour. Most hours show zero activity — that's signal too.
- Manual run → covers everything since the last (manual or scheduled) report.
The first ever report has no baseline and defaults to "last 24h", labelled (first report) in the message.
If a Telegram send fails, the "last report" timestamp is not advanced — so the next attempt picks up the missed window.
- Down: ~100 MB GET from Cloudflare's
speed.cloudflare.com/__down. - Up: ~25 MB POST to
speed.cloudflare.com/__up. - Together ~150 MB per test. Run frequency capped by Speedtest Cooldown (hours) (default 6) regardless of cron tightness — so an hourly cron still only tests bandwidth 4× per day.
- Single-stream measurement — less precise than multi-stream tools like Ookla but adequate for daily trend detection. No external binaries required.
The plugin writes a small .state.json file alongside its code (e.g. /data/plugins/telegram-alerts/.state.json) to track last_report_at and last_speedtest_at. Losing this file (manual delete, container rebuild) is non-fatal — the next report falls back to the 24h default window.
- Fix:
Source:field reported the wrong provider forstream_switch(and any reconnect that landed on a non-primary stream). The plugin was calling its own_lookup_stream_source(channel_name)which always returned the channel's priority-1 stream's M3U account — regardless of which provider Dispatcharr was actually playing. Real symptom: a Comedy Central channel rotated from RM-Realshare to RM-Prada via astream_switch, the Stats page correctly showed[RM-Prada], but the Telegram alert reportedSource: RM-Realshare. Now readspayload["provider_name"], which Dispatcharr'sdispatch_event_systemalready populates by resolving the currently activestream_idto its M3U account name. The old lookup is preserved as a fallback for events without stream context.
- Fix: Daily report never actually sent. Same upstream Dispatcharr bug as Dispatcharr#1244: plugin
@shared_taskregistrations don't propagate to Celery's default-queue prefork pool children, so the daily report task — dispatched correctly by beat at the configured cron time — was rejected by every worker withKeyError: 'telegram_alerts.send_daily_report'. Thetotal_run_counton the periodic task still ticked up, so Show Status looked healthy. As a workaround the plugin now routes its scheduled task to thedvrqueue (single-process thread pool — does register the task correctly). Dispatcharr#1245 is a partial upstream fix; a follow-up usingworker_process_initinstead ofworker_readyis needed to remove this workaround. - Fix: Show Status
last_run_atwas meaningless. django-celery-beat only updates that field on dispatch, not on completion — so the timestamp moved even when the worker rejected the task. The task now bumpslast_run_atitself on successful completion. A stale timestamp now correctly indicates a failing schedule. - Upgrade note: if you set up your schedule on 0.4.3 or earlier, click Apply Schedule once after upgrading — that rewrites the stored task to add
queue='dvr'. Without it, beat will keep dispatching to the default queue (where the worker rejects the task).
- Geo lookup switched from
ipapi.cotoipinfo.io— ipapi was returning HTTP 403 after a handful of probes (likely free-tier per-IP rate limit). ipinfo gives 50k/month per IP with no auth. - Network section now includes a country flag emoji derived from the 2-letter country code (e.g. 🇬🇧 London, England). The full country name is no longer rendered — the flag carries the same information and is more visually scannable.
- Message format tightened: em-dash separates IP from details, middot separates location from ISP.
- Fix: Speedtest download was still failing in v0.4.1. Cloudflare's
/__downrejects single requests over ~75 MB with HTTP 403 (anti-abuse), separate from the User-Agent filter v0.4.1 fixed. Now chunks the download into 4×25 MB sequential GETs — same pattern browser-based speed tests use. Aggregate Mbps = total bytes / total elapsed.
- Fix: Activity section was always empty.
_collect_activity_statsqueriedSystemEvent.created_at, but the actual field isSystemEvent.timestamp. The bareexceptswallowed theFieldErrorand the section silently rendered "(no data available)". - Fix: Speedtest download was always (failed). Cloudflare's
/__downendpoint returns HTTP 403 to non-browser User-Agents. Now uses a Mozilla UA for the speedtest endpoints (and the IP/geo lookups for consistency). - New setting Report Timezone: IANA timezone name (e.g.
Europe/London). Cron is now interpreted in this timezone instead of always UTC. Blank = UTC (unchanged default).
- New optional daily report feature: cron-driven digest covering public IP + geographic location, Cloudflare speedtest (down/up), activity stats (channel plays, top 3 channels, VOD plays, errors, stream switches), and source health (M3U accounts, EPG freshness).
- Activity window = "since previous report" — same cron pattern works at any interval from hourly to weekly.
- Speedtest cooldown setting decouples bandwidth use from cron frequency.
- All sections are individually toggleable. None of the existing per-event alert toggles affect the report's stats counting.
- New actions: Send Report Now, Apply Schedule, Show Schedule Status, Remove Schedule. The scheduling uses
django-celery-beatthe same way VOD2MLIB does.
- New events: subscribes to
vod_startandvod_stop, with their own opt-in toggles. Both off by default. - VOD alerts use Title: for the headline (from
content_name) instead of Channel:. - Include Stream Source now also enriches VOD alerts — looks up the M3U account via Movie/Episode/Series relations.
- The Now-playing EPG enrichment is intentionally suppressed for VOD events (VODs don't have EPG data).
- Fix: Include Stream Source now returns the channel's highest-priority configured stream (the one shown first in Dispatcharr's UI). Previously it returned an arbitrary stream — usually the lowest Stream PK, which on multi-source channels was rarely the user's #1.
- New optional enrichment toggles: Include Stream Source (M3U account name) and Include Current EPG Program (now-playing title). Both off by default; each adds one DB lookup per event when on.
- Lookups degrade silently — a missing EPG mapping or DB hiccup omits the line rather than breaking the alert.
- Initial release.
- Manual
Send Testaction. - Event-driven alerts for
channel_start,channel_stop,channel_reconnect,stream_switchwith per-event toggles. - HTML and plain-text message formats.
- Per-instance label prefix.
MIT — see LICENSE.
