tapo-monitoring
A lightweight, config-driven reliability and alerting stack for TP-Link Tapo PTZ cameras. It combines camera control, on-device detection events, event-aligned frame selection, optional local scoring, Telegram delivery and layered health monitoring in one tested Python daemon.
Project status: the core daemon and alert pipeline are operational and validated on the C560WS, and the
hubpollbattery-camera path is validated end to end against real hub-backed hardware. The Camera Digital Twin (layered health, drift and allow-listed self-healing) and the Shadow Detection Auditor run in production behind opt-in flags; a nightly recorder scan measures camera miss candidates. Camera models and firmware differ; unsupported capabilities degrade tounknowninstead of being guessed.
Tested cameras
| Camera | How it is used | Notes |
|---|---|---|
| C560WS | reference model, runs the whole pipeline | event_profile: default |
| C260 | getEvents with an SD card |
event_profile: default; anonymized report |
| C545D (dual lens) | getEvents with an SD card, per-lens RTSP streams |
needs event_profile: c545d; see dual-lens cameras |
| C410 / C460 on an H200 hub | hubpoll reads detections off the hub |
see battery cameras on a hub |
Other Tapo models with third-party access and getEvents may work; what we learned about
the local API across models is in the local API reference.
Own a model not listed here? Run tapo-monitor report and share the anonymized file:
see camera reports.
What's new in 0.8
- Home Assistant over MQTT — an opt-in, publish-only bridge with MQTT discovery: person and motion sensors, reachability, privacy mode, "detection off", health and the last alert per camera, for automations (MQTT and Home Assistant).
- Your camera model, supported from your report —
tapo-monitor reportwrites an anonymized report of what your camera answers; share it through the Camera report issue form (camera reports).
What's new in 0.7
- Told when a camera stops watching — one Telegram message when privacy mode, motion
or person detection is switched off or back on (
privacy_notice,detection_notice), andfollow_app_notificationsto go quiet while the Tapo app's notifications are off (configuration). - One visit, one message — an SD follow-up is held to the cooldown when the camera already alerted live.
- Dual-lens C545D — per-lens self-heal, the lens and the number of people in the caption, and a client-side deny-list for the calls that drop the camera's API.
- Collect-only mode —
telegram_alerts: falserecords alerts without sending them.
What's new in 0.6
- Quality per visit, not per frame —
label-statsgroups frames into incidents and reports missed visits and the delay to the first alert by delivery path (incidents). - Labelling workflow — a frame collector, a teacher model that pre-labels the easy frames and a local labelling page (labeling); a small random sample of below-threshold frames keeps possible misses in the queue.
- Opt-in alert tuning — a separate
scorer.night_threshold, and asampler.hold_expirypolicy for held frames whose corroboration never came (configuration). - Faster follow-ups — recording follow-ups are read sooner and look at the event's opening seconds first, frames are scored concurrently and while ffmpeg extracts them, and SD/recording reads run off the main loop so one camera's card no longer stalls the others.
- The photo worth sending —
sd_frame_pick: largestsends the frame where the person is closest, within a blur guard. - Disk and card health — the daily digest reports the frame logs' disk use and warns
when the disk runs low; a counterfeit SD card (
dilatant_suspect) is flagged by the digital twin. - Dual-lens C545D support, and a public local API reference.
Details and upgrade notes: CHANGELOG.
Why this project exists
pytapo provides camera API operations.
HomeAssistant-Tapo-Control
exposes a broad Tapo feature set in Home Assistant. Frigate
is a complete local AI NVR. This project occupies a narrower space:
- run on modest hardware without requiring continuous local video inference;
- use the camera's on-device AI as the primary event source;
- encode Tapo-specific reliability knowledge such as login lockouts, sequential requests, SmartTrack ordering and SD recording freshness;
- treat an alert as successful only after the notification is delivered;
- distinguish network, API, events, RTSP and storage failures;
- measure desired configuration drift and, nightly, camera detection misses.
It is not a full NVR, video-management UI or replacement for Frigate. An optional local recorder and scorer can complement the daemon, but neither is required for the basic flow.
Highlights
- One fleet daemon — multiple cameras, one validated YAML configuration and one state machine per camera.
- Low-login event polling —
getEventsis polled every few seconds on an existing session while reconnect/control runs on a slower interval. - Firmware-safe camera control — day/night policy, people-only SmartTrack, presets, rain gating, sensitivity control and optional ONVIF soft pan limits.
- Hardware-aligned optics & safety — on-sensor Lens Distortion Correction (LDC) straightens wide-angle 4K frames for sharper peripheral YOLO scoring, tamper monitoring is self-healed every control pass, and white floodlight activation duration scales down to polite 30 s pulses instead of the 5-minute firmware default.
- On-device face intelligence —
learn-faceCLI helper captures on-device face enrollment events, andfaces.ignore_knownquiets alerts for familiar household members. - Reliable event media — live RTSP first, then an event-time SD-card or local-recorder follow-up when the first frame misses the subject; the subject zoom can be cropped from a native-resolution grab so a distant figure stays legible.
- Battery cameras on a hub —
hubpollreads a sleeping camera's detections off the hub it records to; with a scorer, the sharpest accepted frame is selected from the stored clip. A go2rtc sidecar supplies a live fallback when the clip cannot be decoded. - Optional local scorer — a small HTTP YOLO service can gate frames and return subject boxes; Groq remains optional caption enrichment.
- Confirmed Telegram semantics — failed sends do not arm cooldowns; recovery, SD and sampler paths remain retryable.
- It tells you when it stops working — a dead-man's switch outside the main loop reports a daemon whose every tick is failing, because silence otherwise looks exactly like a quiet night.
- Event-API watchdog — separates
getEventsfailures from network reachability, reports recovery and can request one bounded API reboot per failure episode. - Daily fleet heartbeat — one digest a day says the fleet is alive: cameras, tick, scorer, recorder, alert counts. It claims OK only for what it checked, and any failed check removes the headline.
- Verifiable deploys — releases ship as fingerprinted directories behind a symlink, so rollback is re-pointing the link; an opt-in localhost-first JSON status endpoint answers "how is this host doing" without log parsing.
- Camera Digital Twin — redacted safe-getter snapshots, desired/actual drift and layered health without a second login loop.
- Shadow Detection Auditor — a private, media-free SQLite ledger correlates camera events with independent scorer/recorder observations.
- One owner for the motor — preset recalls and the pan-limit guard ask one arbiter before moving, so the guard never fights a tracked subject or a lens parked by privacy mode.
- Restart-safe, traceable alerts — queued alerts and cooldowns survive a deploy;
every alert carries an incident ID you can follow with
tapo-monitor incident, andtapo-monitor replaychecks a config change against a recorded night before it ships. - Thresholds from your own frames, not guesswork — a collector keeps every host's sent and held frames beyond their retention, a larger teacher model labels the frames it and the scorer agree on, and a local page shows you only the disagreements. On the first pilot pass this turned 815 frames into 86 clicks, and showed that most frames held back for corroboration were real people — the kind of finding a threshold tweak by feel never produces.
See the complete capability catalog and architecture.
Quick start
Requirements: Python 3.10+, ffmpeg, a supported Tapo camera with third-party access, and
camera/RTSP credentials created in the Tapo app.
Install the released package from PyPI (add extras as needed: [mqtt] for Home
Assistant, [scorer] for the local scorer, [onvif] for the pan limit):
python3 -m venv ~/tapo-monitor/.venv
source ~/tapo-monitor/.venv/bin/activate
pip install tapo-monitor # or: pip install 'tapo-monitor[mqtt]'
curl -fsSLo cameras.yaml \
https://raw.githubusercontent.com/PeterkoCZ91/tapo-monitoring/main/cameras.example.yaml
$EDITOR cameras.yaml
For development, work from a clone instead:
git clone https://github.com/PeterkoCZ91/tapo-monitoring.git
cd tapo-monitoring
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
cp cameras.example.yaml cameras.yaml
$EDITOR cameras.yaml
Then, either way:
export TELEGRAM_TOKEN=...
export TELEGRAM_CHAT_ID=...
export CAM_USER=...
export CAM_PASSWORD=...
export CAM_RTSP_USER=...
export CAM_RTSP_PASSWORD=...
tapo-monitor check cameras.yaml
tapo-monitor run cameras.yaml
Prefer containers? docker compose with the shipped Dockerfile,
docker-compose.example.yml and tapo.env.example gets the same daemon running without a
local Python setup, on amd64 or a 64-bit Raspberry Pi — see Docker.
The YAML contains environment-variable names, never secret values. cameras.yaml,
.env files and runtime media are git-ignored. Start from the
configuration guide, not by enabling every optional feature.
Architecture at a glance
flowchart LR
CAM["Tapo camera"] -->|"getEvents"| DAEMON["tapo-monitor daemon"]
CAM -->|"RTSP / SD recording"| MEDIA["frame selection"]
DAEMON --> MEDIA
MEDIA -->|"optional HTTP"| SCORE["local scorer"]
SCORE --> DECIDE["alert decision"]
MEDIA --> DECIDE
DECIDE -->|"optional caption"| GROQ["Groq"]
DECIDE --> TG["Telegram"]
DAEMON --> HEALTH["uptime + layered health"]
DAEMON --> TWIN["Digital Twin + drift"]
DAEMON --> LEDGER["Shadow audit ledger"]
REC["independent recorder/scorer"] --> LEDGER
The fast event path and slower control/reconnect path are intentionally separate. A camera that is pingable can still have a broken API, stale events or failed RTSP, so those layers are observed independently. See Architecture for timing, failure containment and persistence.
Event decision flow
- Poll
getEventsand decode the firmware'sevents_1bitmask. - Treat a camera-confirmed person differently from bare motion.
- Capture a live RTSP frame and optionally score it locally.
- If a confirmed event's frame is empty, queue an event-aligned SD/recorder follow-up.
- Optionally crop and caption the chosen frame.
- Send Telegram and advance cooldown state only after confirmed delivery.
- Record the branch in the structured audit stream and optional private ledger.
This separation matters: a camera event says that something happened, not that the first available frame contains the subject.
CLI
| Command | Purpose |
|---|---|
tapo-monitor check [cameras.yaml] |
Validate configuration and print the fleet summary. |
tapo-monitor version |
Release plus a fingerprint of the deployed module set. |
tapo-monitor selfcheck [cameras.yaml] |
Can this host run? Imports, config, credential env vars, ffmpeg. |
tapo-monitor run [cameras.yaml] |
Start the daemon. |
tapo-monitor status [health.json] |
Inspect observed uptime, outages and reconnects offline. |
tapo-monitor audit-log [logfile|-] |
Summarize event/scorer/Telegram audit records. |
tapo-monitor twin-status [twin.json] [--json] |
Inspect layered health and config drift offline. |
tapo-monitor probe [cameras.yaml] [--camera N] [--json] |
Probe cameras now; opens its own authenticated session. |
tapo-monitor report --host IP [--out FILE] [--watch S] [--rtsp] |
Anonymized, shareable report of what your camera model answers; see camera reports. |
tapo-monitor shadow-record ... |
Ingest one independent media-free observation. |
tapo-monitor shadow-report ... |
Correlate camera and shadow observations. |
tapo-monitor shadow-scan ... |
Nightly recorder audit: re-score yesterday's segments for miss candidates. |
tapo-monitor incident <camera>-<start> |
Everything recorded for one incident: ledger rows and sent-log frames (read-only). |
tapo-monitor replay cameras.yaml ... |
Replay a recorded ledger window through the alert gates and recorded SD/sampler/hub deliveries (read-only); --compare a candidate config or threshold, --scene-reach to measure the scene gate. |
tapo-monitor label <dataset_dir> [--port 8791] [--bind 127.0.0.1] |
Label collected sent/review-log frames in a local web page; verdicts go to labels.jsonl. |
tapo-monitor label-stats <dataset_dir> [--config cameras.yaml] [--json] |
False-alarm rate, misses and the best-separating threshold from the labels; per incident: missed visits and the delay to the first alert by delivery path; with --config, split by day and night. |
tapo-monitor autolabel <dataset_dir> --model yolox_x.onnx |
A larger teacher model auto-labels frames it and the scorer agree on; the page then shows only disagreements. |
Optional local scorer
Install scorer dependencies and run the stateless HTTP service on the same host or a stronger machine:
pip install 'tapo-monitor[scorer]' # from a clone: pip install -e ".[scorer]"
python -m tapo_monitor.scorer_service --model /path/to/model.onnx --port 8766
Then set the camera's scorer.url. If the scorer is unavailable, the alert pipeline
degrades to unfiltered passthrough rather than silently dropping camera-confirmed people.
The configured threshold gates on person confidence; animal confidence never sends an
alert by itself, but a confident animal score marks the caption with a paw so a dog walker
is not indistinguishable from a lone figure. Tiled inference and optional subject cropping
help with distant subjects in wide views. The scorer also exposes
aggregate-only runtime counters at /metrics for operational monitoring.
Two opt-in calibration aids help tune thresholds by frame rather than by guesswork: an
archive of every photo sent to Telegram (TAPO_SENT_LOG_DIR, self-pruning) and
python -m tapo_monitor.scene_probe, which scores a live frame internally without alerting.
See Operations. To calibrate from ground
truth, tools/collect_frames.sh gathers every host's frames into one dataset,
tapo-monitor autolabel pre-labels the easy ones with a larger teacher model and
tapo-monitor label serves the rest on a local page; label-stats then reports the
false-alarm rate, the misses and the best-separating threshold, by day and night with
--config, and per incident how many visits with a person got no alert and how late the
first alert came on each delivery path (Labeling).
Home Assistant (MQTT)
An opt-in, publish-only MQTT bridge announces every camera to Home Assistant through MQTT
discovery: person and motion binary sensors, reachability, privacy mode, "detection off",
twin health and the last alert's time and score, plus the daemon's own availability.
Alert photos are published only with publish_images: true. Install the extra and add
the block:
pip install 'tapo-monitor[mqtt]'
mqtt:
host: broker.example.org
user_env: MQTT_USER
password_env: MQTT_PASSWORD
The bridge runs on its own thread with a bounded queue, so a slow or unreachable broker never delays an alert. Entities, topics and an example automation: docs/mqtt.md.
Safety and privacy
- No credentials, coordinates, camera addresses or face mappings belong in the repository.
- Runtime state is stored under the XDG state directory with private file permissions.
- Digital Twin snapshots are recursively redacted and use a fixed safe-getter allow-list.
- The Shadow ledger stores timestamps and normalized metadata, never frames or stream URLs.
- Siren, floodlight and speaker actuators are intentionally not implemented.
- Repeated authentication failures use backoff because affected firmware can lock out the source address.
Do not run multiple camera-control integrations against the same device unless you understand the session and sequential-request limitations.
Documentation
| Document | Start here when… |
|---|---|
| Documentation index | You want a map of all public docs. |
| Configuration | You are preparing cameras.yaml and environment variables. |
| Architecture | You want component boundaries, timing and failure semantics. |
| Operations | You are deploying, monitoring or calibrating a live instance. |
| Docker | You want to run the monitor (and optional scorer) in containers. |
| Capabilities | You need the implemented/planned feature inventory. |
| Observability | You are enabling Digital Twin or Shadow Auditor. |
| MQTT and Home Assistant | You want the cameras as Home Assistant entities. |
| Labeling | You want ground truth for collected alert frames. |
| Troubleshooting | You hit authentication, RTSP, SD or firmware-specific problems. |
events_1 bitmask |
Your firmware returns incomplete event types. |
| Local API reference | You call the camera's local API yourself: conventions, error codes, SD card, events, calls to avoid. |
| Roadmap | You want current gaps and planned product phases. |
Development
pytest -q
ruff check .
Pure planning, classification, drift, matching and persistence logic is tested without
camera hardware; I/O collaborators are injected in tests. CI runs tests and Ruff on
Python 3.12 and 3.13 (the versions the fleet runs; requires-python stays 3.10+) for
every push to main and every pull request.
See Contributing, Security, the changelog and the MIT license.
Metadata
Release files for tapo-monitor 0.8.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tapo_monitor-0.8.1.tar.gz | 511.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tapo_monitor-0.8.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 812.5 kB
Release files / tapo_monitor-0.8.1.tar.gz
| Download URL | tapo_monitor-0.8.1.tar.gz |
|---|---|
| Size | 511.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6f2946352328760cd10024b9360c3e5bcc3480771f92335d2c6b24b3d8f276c4
|
|
BLAKE2b-256 checksum How to use checksums |
9605f9e6ba54e18adfec77bbacba8bccb6c32af4c548d65b63bb14172bb4f8d2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency logRelease files / tapo_monitor-0.8.1-py3-none-any.whl
| Download URL | tapo_monitor-0.8.1-py3-none-any.whl |
|---|---|
| Size | 301.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e6d4bd76fdcfb58bcd80448c6186a7ed3f8ec54d9fcb47722f8778a6487934d8
|
|
BLAKE2b-256 checksum How to use checksums |
787e28b4631f4af1759abbd411f5a41d6875a17d869a7df5a90234ef4e237f4e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency log