Local-first probe and Prometheus exporter for TP-Link Tapo P110 smart plug energy readings.
The tool can run once to append JSONL readings, or run continuously as a local exporter for Grafana Alloy and Grafana Cloud.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e '.[dev]'Create .env for your Tapo cloud account credentials. The local Tapo protocol library uses these credentials to authenticate to plugs on your LAN. .env is ignored by git.
TAPO_USERNAME=you@example.com
TAPO_PASSWORD=your-passwordShell environment variables with the same names override .env values.
In the Tapo app, enable local third-party access: Me > Third-Party Services > Third-Party Compatibility. Without this, plugs can be discovered but local energy reads can fail with FORBIDDEN.
Create a local config file. tapo-config.json is ignored by git.
{
"devices": [
{"name": "desk", "hostname": "desk-plug", "ip": "192.168.1.50"}
]
}Fields:
name: stable local name for CLI output and metric labels.ip: plug LAN IP address.hostname: optional dashboard-friendly label. If omitted, the exporter uses the Tapo nickname when available, thenname.
If you do not know plug IP addresses, check router DHCP leases, a LAN scanner, or the Tapo app device details. Configure static DHCP reservations once found so dashboard labels remain stable.
You can also scan for devices:
tapo-probe --discoverRun a probe and append readings to JSONL:
tapo-probe --config tapo-config.json --output tapo-readings.jsonlEach JSONL line contains timestamp, configured device name, IP, normalized power fields, and raw fields useful for troubleshooting firmware differences.
Run the exporter locally:
tapo-probe serve --config tapo-config.json --port 9108 --interval 60Metrics are exposed at http://localhost:9108/metrics. Each metric includes hostname, ip, name, nickname, and alias labels.
Primary metrics:
tapo_plug_power_wattstapo_plug_today_energy_whtapo_plug_month_energy_whtapo_plug_today_runtime_secondstapo_plug_month_runtime_secondstapo_plug_rssi_dbmtapo_plug_uptapo_plug_on_time_secondstapo_plug_signal_leveltapo_plug_overheattapo_plug_overcurrenttapo_plug_power_protection_triggeredtapo_plug_info
Dashboard-compatible metrics are also emitted:
tapo_energyUsage_currentPowerin milliwattstapo_energyUsage_todayEnergytapo_energyUsage_monthEnergytapo_energyUsage_todayRuntimetapo_energyUsage_monthRuntimetapo_deviceInfo_rssitapo_deviceInfo_device_on
Run the installer from the repo root:
scripts/install-service.shThe installer is user-level by default. It does not require sudo. On macOS and Linux, the installer manages both the exporter and Grafana Alloy so local collection and Grafana Cloud remote_write survive login/reboot.
What it does:
- Creates
.venvif needed and installs this package. - Creates
~/.config/tapo-probe/tapo-config.jsonwith a sample device if it does not exist. - On macOS, installs
~/Library/LaunchAgents/com.tapo-probe.exporter.plist. - On macOS, installs
~/Library/LaunchAgents/com.tapo-probe.alloy.plistwhenalloyis available. - On Linux, installs
~/.config/systemd/user/tapo-probe.service. - On Linux, installs
~/.config/systemd/user/tapo-probe-alloy.servicewhenalloyis available. - Starts the exporter with
tapo-probe serve --config ~/.config/tapo-probe/tapo-config.json --port 9108 --interval 60. - Starts Alloy with a managed
~/.config/tapo-probe/alloy.configcopied fromgrafana/alloy.config.example. - On Linux, enables lingering with
loginctl enable-linger "$USER"when available, so the user service survives logout and reboot.
The service starts automatically after user login. On Linux user services, the installer enables lingering when possible for headless boot before login. If your OS policy blocks this, run sudo loginctl enable-linger $USER once.
Before relying on the service, edit:
~/.config/tapo-probe/tapo-config.jsonKeep Tapo and Grafana credentials in .env in this repo or set them in the service environment using your OS service tooling. The managed Alloy wrapper sources .env without printing values. Do not put credentials in tapo-config.json.
Install Alloy before running the installer if you want Grafana Cloud remote_write managed automatically:
brew install grafana/grafana/alloyFor Linux package installation options, see the Grafana Alloy Linux install docs: https://grafana.com/docs/alloy/latest/set-up/install/linux/
Installer commands:
scripts/install-service.sh install
scripts/install-service.sh status
scripts/install-service.sh uninstallmacOS service commands:
launchctl print gui/$(id -u)/com.tapo-probe.exporter
launchctl print gui/$(id -u)/com.tapo-probe.alloy
launchctl kickstart -k gui/$(id -u)/com.tapo-probe.exporter
launchctl kickstart -k gui/$(id -u)/com.tapo-probe.alloy
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.tapo-probe.exporter.plist
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.tapo-probe.alloy.plistmacOS logs:
tail -f ~/Library/Logs/tapo-probe.out.log ~/Library/Logs/tapo-probe.err.log ~/Library/Logs/tapo-probe-alloy.err.logLinux service commands:
systemctl --user status tapo-probe.service
systemctl --user status tapo-probe-alloy.service
systemctl --user restart tapo-probe.service
systemctl --user restart tapo-probe-alloy.service
systemctl --user stop tapo-probe.service
systemctl --user stop tapo-probe-alloy.serviceLinux logs:
journalctl --user -u tapo-probe.service -f
journalctl --user -u tapo-probe-alloy.service -fSet a different listen port or poll interval during install:
TAPO_PROBE_PORT=9110 TAPO_PROBE_INTERVAL=30 scripts/install-service.shBuild an exporter-only image with Docker:
docker build -f Containerfile -t tapo-probe:local .Or with Podman:
podman build -f Containerfile -t tapo-probe:local .Run the exporter with a mounted config file and credentials from the environment:
docker run --rm \
-p 9108:9108 \
-e TAPO_USERNAME='you@example.com' \
-e TAPO_PASSWORD='your-password' \
-v "$PWD/tapo-config.json:/config/tapo-config.json:ro" \
tapo-probe:localPodman uses the same arguments:
podman run --rm \
-p 9108:9108 \
-e TAPO_USERNAME='you@example.com' \
-e TAPO_PASSWORD='your-password' \
-v "$PWD/tapo-config.json:/config/tapo-config.json:ro" \
tapo-probe:localThe container runs tapo-probe serve --config /config/tapo-config.json --port 9108 --interval 60 by default. Keep .env and local config files outside the image; .dockerignore excludes common secret and local runtime files from the build context.
For long local retention, run Grafana and Prometheus on the same host as the exporter with Docker Compose:
GRAFANA_ADMIN_PASSWORD='change-this-password' docker compose up -dOpen Grafana at http://cb1.lan:3000 and sign in with user admin and the password from GRAFANA_ADMIN_PASSWORD. Prometheus is published only on localhost at http://127.0.0.1:9090.
The stack provisions Prometheus as the default Grafana data source and loads the sample Tapo dashboard from grafana/tapo-p110-dashboard.sample.json. The Compose services use host networking so Prometheus can scrape the host exporter at 127.0.0.1:9108; keep tapo-probe.service running on the host.
The default Prometheus retention is two years:
--storage.tsdb.retention.time=2yUseful checks:
docker compose ps
curl -fsS http://127.0.0.1:9090/-/ready
curl -fsS http://127.0.0.1:9108/metrics | grep '^tapo_' | headTo stop the local Grafana stack without deleting data:
docker compose downTo delete local Grafana and Prometheus data as well:
docker compose down -vInstall Grafana Alloy and copy grafana/alloy.config.example to your Alloy config path. Set these environment variables from your Grafana Cloud Prometheus remote_write details:
export GRAFANA_CLOUD_PROM_URL='https://prometheus-prod-xx.grafana.net/api/prom/push'
export GRAFANA_CLOUD_PROM_USER='your-prometheus-user-id'
export GRAFANA_METRICS_WRITE='your-grafana-cloud-metrics-write-token'
export GRAFANA_METRICS_READ='your-grafana-cloud-metrics-read-token'GRAFANA_CLOUD_PROM_URL must be the Prometheus remote_write endpoint from Grafana Cloud, not a dashboard URL. Dashboard URLs like https://example.grafana.net/d/ are for viewing dashboards and cannot receive metrics.
GRAFANA_METRICS_WRITE must be valid for Grafana Cloud Metrics remote_write. A Grafana service account token that can call the Grafana dashboard API may still fail remote_write with 401 Unauthorized: invalid token unless it has the Grafana Cloud Metrics publish/write permission. GRAFANA_METRICS_READ is used only for verification queries.
Import grafana/tapo-p110-dashboard.sample.json into Grafana and choose your Prometheus data source. The dashboard uses the compatibility metric names emitted by the exporter, including tapo_energyUsage_currentPower, tapo_energyUsage_todayEnergy, and tapo_deviceInfo_rssi.
Illustrative preview of the sample dashboard:
Useful PromQL examples:
tapo_plug_power_watts
tapo_plug_power_watts{hostname="desk-plug"}
tapo_energyUsage_currentPower / 1000
tapo_plug_up == 0
tapo_plug_overheat or tapo_plug_overcurrent or tapo_plug_power_protection_triggered
pytest -v
bash -n scripts/install-service.sh