A Telegram bot that monitors mains voltage through a Tuya smart plug and reports it to a channel/group: a live pinned status message, a 24-hour chart with day/night tariff bands and power-outage shading, and edge-triggered alerts for low/high voltage.
Built for an area with an unstable grid and scheduled blackouts — the bot answers two questions at a glance: what's the voltage right now? and how long was there no power in the last 24 hours?
- Polls a Tuya plug locally over LAN (
tinytuya, no cloud dependency). - Single self-updating pinned status message + pinned chart (edited in place, not re-posted).
- Outage tracking with a flap debounce: one missed poll is a gap, not a blackout — only a sustained loss counts as downtime.
- Voltage chart: normal band, low/high markers, day/night tariff strip, shaded outage periods.
- Low/high voltage alerts that fire once per transition, not every cycle.
- Pluggable storage: zero-config SQLite by default, PostgreSQL via a single env var.
- Structured logging, graceful shutdown, bounded retries with backoff.
Any Tuya/Smart Life plug that exposes a voltage data point (DPS) over the
local API. You need the device's DEVICE_ID, LOCAL_KEY, and LAN IP
(obtainable with python -m tinytuya wizard). The default decoding assumes
voltage is on DPS 23 in decivolts (raw / 10); override via env if your
device differs.
tuya_client ─► monitor ─► storage (SQLAlchemy: SQLite | PostgreSQL)
│ ▲
│ └── history (pure logic: outage, formatting, segments)
├──► chart (matplotlib)
└──► telegram_client (pinned status + chart, alerts)
config (.env) ─► everything | logging_setup ─► console + rotating file
The interesting logic (outage duration, series segmentation, formatting)
lives in history.py as pure functions with no I/O — easy to reason
about and unit-test in isolation.
src/voltage_bot/
config.py env → validated Settings
logging_setup.py console + rotating file
tuya_client.py tinytuya wrapper (timeout + retry)
models.py SQLAlchemy models
storage.py repositories + one-time history.json import
history.py pure logic (testable)
chart.py matplotlib rendering
telegram_client.py pinned message / chart / alerts
monitor.py main loop, debounce, alert state machine
scripts/run_termux.sh supervised restart loop for Android/Termux
docker-compose.yml optional PostgreSQL service
python -m venv .venv
. .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # then edit .env
python -m voltage_botAll configuration is via environment variables (loaded from .env).
BOT_TOKEN, TUYA_DEVICE_ID, TUYA_LOCAL_KEY, TUYA_IP are required, plus
at least one of CHANNEL_ID / GROUP_ID. See .env.example
for the full annotated list (intervals, voltage thresholds, tariff hours,
timezone, log level, DATABASE_URL).
- Termux / Android (the original target):
bash scripts/run_termux.sh— supervised restart loop with wake-lock and exponential backoff. - Server (systemd): run
python -m voltage_botunder a unit withRestart=on-failure. The process handles SIGTERM cleanly.
The storage layer is a thin SQLAlchemy wrapper; the backend is selected
entirely by DATABASE_URL:
- SQLite (default) — no
DATABASE_URLset. A single transactional file, zero configuration. This is the recommended mode for the real deployment target (Termux on an Android phone), where running a database server is impractical (resources, battery, no service manager). SQLite already gives transactional writes, which removes the corrupt-file failure mode of the old plain-JSON approach. - PostgreSQL (optional) — set
DATABASE_URL=postgresql+psycopg2://voltage:voltage@localhost:5432/voltageanddocker compose up -d. Same code path,timestamptzstorage, indexed time queries — the "server / showcase" deployment.
An existing legacy history.json is imported automatically once, on first
run, if the database is empty.
- Telegram: open
@BotFather→/revoke→ use the newBOT_TOKEN. - Tuya: re-pair the device in the Smart Life app (re-pairing rotates the
local key) or regenerate it in the Tuya IoT console; update
TUYA_LOCAL_KEY. - Put the new values only in a local
.env— never commit it. .gitignoreexcludes.env,*.db, and the legacystate.json/history.json. Add it beforegit initso secrets/data never enter git history.
MIT — see LICENSE.
