A Prometheus exporter for TP-Link Kasa smart plugs and power strips that exposes real-time power consumption, voltage, current, and energy metrics.
This exporter communicates exclusively over the local network (LAN) using direct socket calls via python-kasa. It is specifically designed for devices that are blocked from WAN access / TP-Link Cloud via firewall rules or isolated on dedicated IoT VLANs.
All device communication happens over your internal network. No external API endpoints, no telemetry phone-home, and no dependency on public DNS or internet connectivity. This makes it ideal for:
- Air-gapped environments where smart devices must never touch the internet
- IoT VLAN deployments where devices are firewalled from WAN
- Privacy-first setups that block all TP-Link cloud outbound traffic
- Edge / remote sites with no or unreliable internet connectivity
This exporter does not handle TP-Link Cloud account authentication or remote cloud API polling. Devices must be reachable via their local IP address on your LAN.
While the python-kasa library does support cloud authentication for certain device types, this exporter has not been tested with cloud-authenticated devices and is designed around direct local communication. If your devices require TP-Link Cloud credentials (which require authentication via --username and --password in python-kasa) this exporter would need enhancements to pass them.
Many Prometheus exporters use the Multi-Target Probe Pattern (/scrape?target=192.168.1.X). This breaks standard auto-discovery engines (Grafana Alloy, Kubernetes Prometheus Operator, Docker Swarm label rules) because collectors expect a simple http://<container_ip>:<port>/metrics target.
This container behaves as a standard 12-factor microservice exposing a single /metrics endpoint. Grafana Alloy or Prometheus auto-discovery picks it up immediately via container labels (prometheus-port=9233) without requiring target-relabeling rules. All configured devices are exposed as distinct label sets within the same scrape — no dynamic target injection needed.
Many lightweight exporters poll physical Kasa hardware synchronously when Prometheus initiates an HTTP scrape. If local Wi-Fi drops a frame or the plug delays its response, the scrape times out, resulting in missed metrics or false alerting.
This exporter separates the background asyncio poll loop from the Prometheus HTTP server. A scrape request to /metrics returns in under 5ms directly from in-memory gauges. Wi-Fi hiccups do not cause Prometheus scrape timeouts. The exporter continues polling silently in the background and updates gauges on the next successful poll cycle.
Older standalone exporters written in Go or C often break when TP-Link updates local firmware or switches security protocols (e.g., legacy XOR port 9999 vs. modern KLAP, AES, or SMART protocols).
This exporter relies on python-kasa for all protocol communication, delegating maintenance to the active Home Assistant and python-kasa developer community. When TP-Link changes firmware behavior, a single pip install --upgrade python-kasa in the Docker build restores compatibility. No custom protocol handshakes are maintained in-house.
Simple single-plug exporters fail or only read Outlet 0 on 6-port strips (HS300, KP303) because they do not query child device contexts. Raw TP-Link firmware returns stats in millivolts (e.g., 110000 mV), leaving users with raw values unless they write Prometheus recording rules.
python-kasa handles parent-child socket iteration (strip.children) and normalizes milli-units to standard Volts (V), Watts (W), Amps (A), and kWh. Every child outlet is cleanly labeled with outlet_index and outlet_name, making dashboard queries straightforward without post-processing.
The exporter follows 12-factor app principles. All configuration is delivered via environment variables or a single mounted YAML file, making it easy to deploy as:
- Docker Compose service with
environment:keys - Docker Swarm task with constraints and labels
- Kubernetes Pod with ConfigMap volumes and env refs
- Bare-metal systemd service with
EnvironmentFile=
The YAML volume mount is read-only (:ro) for container security.
All metrics are exposed as Prometheus Gauge types.
| Metric | Description | Labels |
|---|---|---|
kasa_exporter_up |
Whether the exporter is healthy and config is valid (1=up, 0=down) |
(none) |
kasa_device_reachable |
Whether the device responded to the last poll (1=reachable, 0=unreachable) |
device_ip, device_name |
kasa_last_error_code |
Numeric code of the last error encountered per device (0=none, 1=unreachable, 2=timeout, 3=auth_failure, 99=unknown) |
device_ip, device_name, error_type |
| Metric | Description | Labels |
|---|---|---|
kasa_device_info |
Device metadata (static info metric set to 1) |
device_ip, device_name, model, firmware, hardware, mac |
kasa_device_rssi_dbm |
Wi-Fi signal strength in dBm | device_ip, device_name, + global labels |
kasa_device_uptime_seconds |
Device uptime in seconds | device_ip, device_name, + global labels |
| Metric | Description | Labels |
|---|---|---|
kasa_device_power_watts |
Total real-time power draw for entire strip in Watts | device_ip, device_name, + global labels |
kasa_device_voltage_volts |
Main line voltage in Volts | device_ip, device_name, + global labels |
kasa_device_current_amps |
Total current draw across whole strip in Amps | device_ip, device_name, + global labels |
kasa_device_total_kwh |
Total cumulative energy consumption since reboot in kWh | device_ip, device_name, + global labels |
kasa_device_energy_today_kwh |
Today's energy consumption for the entire device in kWh (reset by device clock) | device_ip, device_name, + global labels |
kasa_device_energy_month_kwh |
This month's energy consumption for the entire device in kWh (reset by device clock) | device_ip, device_name, + global labels |
| Metric | Description | Labels |
|---|---|---|
kasa_outlet_state |
Outlet power state (1=ON, 0=OFF) |
device_ip, device_name, outlet_index, outlet_name, + custom labels |
kasa_outlet_power_watts |
Real-time outlet power draw in Watts | device_ip, device_name, outlet_index, outlet_name, + custom labels |
kasa_outlet_voltage_volts |
Outlet voltage in Volts | device_ip, device_name, outlet_index, outlet_name, + custom labels |
kasa_outlet_current_amps |
Outlet current draw in Amperes | device_ip, device_name, outlet_index, outlet_name, + custom labels |
kasa_outlet_total_kwh |
Cumulative outlet energy consumption since reboot in kWh | device_ip, device_name, outlet_index, outlet_name, + custom labels |
kasa_outlet_energy_today_kwh |
Today's energy consumption for this outlet in kWh (reset by device clock) | device_ip, device_name, outlet_index, outlet_name, + custom labels |
kasa_outlet_energy_month_kwh |
This month's energy consumption for this outlet in kWh (reset by device clock) | device_ip, device_name, outlet_index, outlet_name, + custom labels |
All metrics include the following core labels:
device_ip— IP address of the Kasa devicedevice_name— Alias or override name for the device
Additional global labels and per-outlet custom labels can be defined in config.yaml and will be attached to all metrics. Example custom labels from the default config:
environment— Global label applied to all metricssite— Global label applied to all metricsrole— Per-outlet label describing the outlet purposetarget_app— Per-outlet label for application mapping
| Code | error_type label |
Meaning |
|---|---|---|
0 |
none |
No error; device polled successfully |
1 |
unreachable |
Device refused connection or network-level failure |
2 |
timeout |
Connection exceeded KASA_CONNECT_TIMEOUT (default 10s) |
3 |
auth_failure |
Credentials rejected by the device |
99 |
unknown |
Unclassified error |
Configuration is handled via a config.yaml file (default path, override with CONFIG_PATH environment variable).
server:
port: 9233
poll_interval: 10 # Seconds between background polls
# Global extra labels applied to ALL metrics exported by this container
global_labels:
environment: "home-lab"
site: "brampton"
devices:
- ip: "192.168.1.100" # IP address of the device
# host: "kasa-strip.local" # OR hostname (either ip or host is required)
name_override: "Core-Rack-Strip"
outlets:
0:
name: "UPS-Main"
labels:
role: "infrastructure"
target_app: "nut-server"
1:
name: "Proxmox-Node-01"
labels:
role: "compute"
target_app: "proxmox"Each device entry supports the following fields:
| Field | Required | Description |
|---|---|---|
ip |
One of ip or host |
IP address of the Kasa device (e.g., "192.168.1.100") |
host |
One of ip or host |
Hostname of the Kasa device (e.g., "kasa-strip.local"). Useful when devices have dynamic IPs but stable mDNS/DNS names. If both ip and host are provided, host takes precedence for connection. |
name_override |
No | Custom name for the device, used in the device_name label. Defaults to kasa_<address> if not set. |
outlets |
No | Map of outlet index to outlet configuration (name, custom labels) |
The exporter validates config.yaml at startup and exits with a non-zero code on fatal errors:
| Edge Case | Behavior |
|---|---|
| Config file missing | Fatal error, exits with sys.exit(1) |
| Malformed YAML | Fatal error, logs parse details then exits |
| Empty / comment-only config | Fatal error, exits with descriptive message |
Missing devices section |
Warning logged; exporter continues (no devices to poll) |
Device entry missing both ip and host |
Fatal error, exits |
| Invalid IP address format | Fatal error, exits with details (only validated if ip is provided) |
| Variable | Default | Description |
|---|---|---|
CONFIG_PATH |
config.yaml |
Path to the YAML config file |
KASA_CONNECT_TIMEOUT |
10 |
Seconds to wait before marking device connection as timeout |
LOG_LEVEL |
INFO |
Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
docker build -t kasa-exporter .docker run -d \
--name kasa-exporter \
--network host \
-v ./config.yaml:/app/config.yaml:ro \
kasa-exporterThe exporter listens on port 9233 by default. Adjust the server.port setting in config.yaml as needed.
docker pull ghcr.io/adystech/kasa-exporter:latestservices:
kasa-exporter:
image: ghcr.io/adystech/kasa-exporter:latest
networks:
- monitoring # Must match network used by Grafana Alloy / Prometheus
volumes:
- ./config.yaml:/app/config.yaml:ro
deploy:
mode: replicated
replicas: 1
labels:
- prometheus-job=kasa_power_exporter
- prometheus-port=9233
- prometheus-scrape-interval=15s- python-kasa — Local protocol communication with Kasa devices
- prometheus_client — Prometheus metric exposition
- PyYAML — Configuration parsing
MIT