Monitor TP-Link Tapo and Kasa energy-monitoring plugs with pyprom-exporters, Prometheus and Grafana. The stack provisions its datasource, dashboard and alert rules from this repository. It probes devices on scrape by default and keeps monitoring data in persistent Docker volumes.
Already running the older stack? Read Migrating an existing installation before starting these containers. The new stack uses fresh volumes by default and does not migrate old databases.
| Component | Pinned version | Purpose |
|---|---|---|
| pyprom-exporters | 0.2.0 | Concurrent Tapo/Kasa discovery and live measurements |
| Prometheus | 3.15.0 | Time-series storage, scraping and alert evaluation |
| Grafana | 13.2.3 | Provisioned power, energy and health dashboard |
Images are pinned by version and digest in compose.yaml. The exporter replaces
fffonion/tplink-plug-exporter; its metrics and device configuration are different.
The dashboard includes current power, daily/monthly energy, voltage, current, Wi-Fi signal and exporter diagnostics. Only Grafana's built-in panels are used. Datasource, job, exporter, host and device filters make it portable across installations, including devices with identical aliases.
Install uv and Docker with the Compose plugin. The Python utility supports Python 3.11 and newer; uv can provision Python when needed. The Docker host must be able to reach your plugs on the LAN.
git clone https://github.com/andylamp/grafana-tp-link.git
cd grafana-tp-link
uv sync --locked
uv run --locked power-monitor initinit creates a Git-ignored .env with private permissions and a randomly generated Grafana admin password.
It prints no password and preserves an existing .env.
Edit the existing exporter.exporters.tapo.devices list in config/stack.yaml,
keeping the file's other settings. Add one plug IP address or resolvable hostname per entry, with comments as needed:
exporter:
exporters:
tapo:
devices:
- "192.0.2.10" # Desk plug; replace this example address.
- "kitchen-plug.example" # Kitchen plug; replace this example hostname.Use your own reachable addresses and reserve their DHCP addresses where possible. Docker bridge networking does not provide reliable LAN broadcast discovery. The comments describe your inventory; dashboard aliases still come from the plugs themselves.
Edit .env for credentials:
- Set
TP_LINK_USERNAMEandTP_LINK_PASSWORDwhen your devices require Tapo account authentication. Older Kasa/HS110 devices may work with both empty. - Keep or change the generated
GRAFANA_ADMIN_PASSWORD. Read it locally from.envwhen signing in. Single-quote values containing$or#so Compose treats those characters literally.
If you already have an exporter-only .env, also add a nonempty GRAFANA_ADMIN_PASSWORD for the stack.
init preserves existing files and does not fill in missing credentials. Both check and up require this value;
an empty GRAFANA_ADMIN_PASSWORD in your shell overrides a value in .env, so unset an unintended shell override.
Then validate and start the stack:
uv run --locked power-monitor check
uv run --locked power-monitor upOpen Grafana using grafana.bind_address and grafana.port from config/stack.yaml
(http://localhost:3333/ with the checked-in configuration),
and sign in as admin, or your configured GRAFANA_ADMIN_USER.
The TP-Link · Power & Energy dashboard is provisioned automatically and selected as the home dashboard.
No API registration script, manual datasource creation or dashboard upload is required.
check validates the effective YAML and environment settings, generates native service configuration, and runs
the pinned Prometheus image's promtool against the configuration and alert-rule fixtures.
It does not start the exporter or probe plugs.
up waits for container health checks; those checks establish service readiness,
not that every configured plug has produced a reading.
Both commands show progress through configuration loading and validation, along with the effective device count,
live/polling mode, and Prometheus scrape interval and timeout. check also identifies each Prometheus validation step.
After a successful up, the utility reports service health, Grafana's home/dashboard URLs, and commands for status/logs.
URLs use Docker's actual published ports, including ports changed or assigned dynamically through Compose overrides.
Prometheus and exporter links appear when their HTTP ports are published; otherwise they are identified as internal
services. The default stack publishes only Grafana. With a remote Docker context, these addresses belong to the Docker
host; use that host or an SSH tunnel to reach them.
Startup failures retain Docker's exit status and print diagnostic commands instead of a success summary.
Credentials are not included in the progress messages or summary.
Run commands from this checkout. From another directory, specify both the uv project and stack location:
uv run --locked --project /path/to/grafana-tp-link \
power-monitor --directory /path/to/grafana-tp-link status| Command | Behavior |
|---|---|
uv run --locked power-monitor init |
Create .env once without replacing local settings |
uv run --locked power-monitor check |
Validate Compose, Prometheus configuration and rules |
uv run --locked power-monitor up |
Create/update services and wait for health checks |
uv run --locked power-monitor status |
Show service status |
uv run --locked power-monitor logs --follow exporter |
Follow exporter logs |
uv run --locked power-monitor down |
Stop this stack while retaining persistent volumes |
uv run --locked power-monitor reset --yes |
Delete this project's containers and data volumes |
uv run --locked power-monitor pull |
Download the pinned images |
make init, make up, make down, make status, make logs and make pull provide equivalent shortcuts.
The compatibility wrapper ./grafana-tp-link-docker -i starts services and -r stops them while preserving data.
Use power-monitor init first. The wrapper's old destructive removal behavior has been removed;
deleting monitoring data now requires the explicit reset command below.
The Compose project is tp-link-monitoring, distinct from the legacy grafana-tp-link project.
status, logs, down, reset --yes and pull also work without .env or a valid config/stack.yaml.
If you originally set a custom
COMPOSE_PROJECT_NAME, retain that name in .env or export it in your shell to address the same stack.
check and up read the YAML and effective environment and require at least one explicit device host
before creating containers.
For a deployment with additional Compose files, pass each one before the command, in the same order every time:
uv run --locked power-monitor --compose-file compose.override.yaml up
uv run --locked power-monitor --compose-file compose.override.yaml downPaths are relative to the checkout; repeat --compose-file for additional files. The utility loads the base
compose.yaml, then generated storage settings for check/up, then explicit overrides in the supplied order.
Retain override options for check, status, logs, reset and other lifecycle commands too.
Pulling downloads the versions recorded in compose.yaml; it does not select newer releases automatically.
Review version and digest changes together, consult upstream upgrade notes, back up data, then run pull and up.
For managed Docker volumes, reset deletes Prometheus history and Grafana's stored accounts and settings. Use it when you intend to discard this stack's persistent data, such as during local iteration:
uv run --locked power-monitor reset --yes
uv run --locked power-monitor upThe equivalent Makefile shortcut supplies the required confirmation flag for you:
make reset && make upreset requires --yes and has no interactive prompt; without that flag it refuses to proceed.
It runs docker compose down --volumes --remove-orphans for the selected Compose project, removing its containers
(including orphaned services), non-external networks, declared named volumes and attached anonymous volumes.
It does not run a global Docker prune or restart services automatically.
Your .env, tracked configuration, source files and downloaded images remain in place.
External volumes and host bind-mounted directories are retained. With the standard named-volume configuration,
the next up creates empty storage and provisions the checked-in dashboard and datasource again, using the admin
credentials retained in .env. A deployment using bind-mounted data keeps that data; reset does not erase its host files.
Reset also works if .env is missing. Preserve or export a custom COMPOSE_PROJECT_NAME to select the intended project;
without that setting, the default project is selected. Recreate/configure .env before starting services again if needed.
For an ordinary stop that retains monitoring data, use down or the wrapper's -r instead.
config/stack.yaml is the single user-facing file for device inventory, exporter behavior,
Grafana's address/port, both services' storage locations, Prometheus retention, and scrape/evaluation intervals.
Credentials stay in the private .env.
Edit the existing YAML values and keep comments next to settings or individual device hosts.
| YAML setting | Checked-in value | Meaning |
|---|---|---|
grafana.bind_address |
localhost |
Host address serving Grafana; normalized to 127.0.0.1 |
grafana.port |
3333 |
Host port serving Grafana |
grafana.data_directory |
null |
Docker volume; set a host directory to use a bind mount |
prometheus.data_directory |
null |
Docker volume; set a host directory to use a bind mount |
prometheus.retention_time |
3y |
Maximum stored history by age |
prometheus.retention_size |
10GB |
Maximum stored blocks by size |
prometheus.scrape_interval |
30s |
Time between exporter scrapes |
prometheus.scrape_timeout |
25s |
Maximum wait for one exporter scrape |
prometheus.evaluation_interval |
30s |
Time between alert evaluations |
exporter.prometheus_port |
8090 |
Exporter's internal metrics port |
exporter.log_level |
INFO |
Exporter logging level |
exporter.exporters.tapo.devices |
Empty list | One explicit plug IP or hostname per entry |
The exporter section contains the exporter's native configuration, including device concurrency, discovery options,
live-refresh settings and metric definitions. It is nested under exporter so the service settings remain in one file.
The utility validates shared settings and common device options.
The exporter validates advanced native options at startup.
Do not put account credentials in YAML. To use other credential environment names, set
discovery_options.tapo_username_env_key and discovery_options.tapo_password_env_key under exporter.exporters.tapo,
then supply those variables in .env or the shell. Names must be environment identifiers (letters, digits and underscores,
starting with a letter or underscore). The renderer forwards references to Compose; credential values stay out of YAML.
.env credential |
Default | Meaning |
|---|---|---|
TP_LINK_USERNAME, TP_LINK_PASSWORD |
Empty | Device account credentials when required |
GRAFANA_ADMIN_USER |
admin |
Initial Grafana administrator name |
GRAFANA_ADMIN_PASSWORD |
Generated by init |
Initial password for a new Grafana database |
Changing Grafana's initial admin settings does not reset accounts in an existing database.
Use Grafana's account-management or password-reset facilities for an existing installation.
Docker administrators can inspect container environment variables; keep .env private.
check and up generate read-only native service files under ignored .runtime/<digest>/.
The utility does not rewrite the selected YAML, so inline comments and formatting survive.
Generated files contain no account credentials and should not be edited or committed.
The tracked Prometheus and Grafana provisioning files are implementation templates; ordinary configuration changes
belong in config/stack.yaml. The renderer keeps the exporter target and Grafana datasource interval aligned.
After changing YAML or credentials, use:
uv run --locked power-monitor check
uv run --locked power-monitor upChanges to generated service files produce different mount paths; port and retention changes update the Compose model.
up recreates affected service containers while retaining their data volumes. No manual restart is needed.
Use these utility commands to prepare configuration before starting services; a raw docker compose up skips that
preparation. Include your --compose-file options when using local overrides.
Grafana binds to the local host by default; localhost is normalized to 127.0.0.1. For remote access, configure a
suitable bind address and your network's access controls, or place it behind an authenticated TLS reverse proxy.
Prometheus and the exporter are available only on the Compose network; their ports are not published to the host.
Prometheus removes older blocks when either retention limit is reached. The size setting does not cap all disk use:
allow additional space for the write-ahead log, active samples and compaction. Volumes persist across down and up.
Use the explicit reset command only when you intend to discard that data.
The default is config/stack.yaml. For custom configurations, keep complete YAML copies in scratch/configs/.
The entire scratch/ directory is excluded by both .gitignore and .dockerignore, keeping local profiles
out of commits and Docker build contexts.
From the repository root, create a profile:
mkdir -p scratch/configs
cp config/stack.yaml scratch/configs/home.yamlEdit scratch/configs/home.yaml to configure your device hosts, storage directories, ports and scrape settings.
Keep its grafana, prometheus and exporter sections; inline comments can describe local settings and devices.
Credentials remain in the checkout's .env or shell environment. If .env does not exist yet, run
uv run --locked power-monitor init and configure credentials as described in Quick start.
Validate and start using the global --config option before the command:
uv run --locked power-monitor --config scratch/configs/home.yaml check
uv run --locked power-monitor --config scratch/configs/home.yaml upCreate additional copies such as scratch/configs/lab.yaml as needed, and pass the chosen file explicitly each time.
Profiles are not discovered automatically. Supported nonempty environment overrides still take precedence over
profile values; remove old overrides when you want the YAML values to apply.
For a single local configuration, config.local.yaml in the repository root is also Git-ignored and can be selected
with --config config.local.yaml.
Relative configuration paths are resolved against the checkout selected by --directory, not the shell's current directory.
Absolute paths also work. Only the selected YAML is loaded; missing or invalid selections never fall back to the default.
Use the same selection for subsequent check and up commands. Credentials still come from that checkout's .env or shell.
The ignored generated configurations remain under that checkout's .runtime/.
Relative storage paths also use the checkout root: ./data/grafana means <checkout>/data/grafana even for a profile
in scratch/configs/.
The old config/exporter.yaml has been consolidated into the stack YAML's exporter section.
Its native settings are preserved when generating the configuration mounted inside the exporter container.
A native exporter-only YAML is not a complete stack configuration; put its contents under exporter alongside grafana
and prometheus. The utility never implicitly reads or merges a leftover config/exporter.yaml.
Selecting another YAML changes settings for the same Compose project; it does not create an isolated deployment.
Use distinct COMPOSE_PROJECT_NAME values and distinct storage when running independent deployments.
Recovery commands can accept --config but do not read it, so broken or missing YAML cannot block stopping the project.
Set grafana.data_directory and prometheus.data_directory in the selected YAML. With null (the default), Docker
manages the existing grafana-data and prometheus-data volumes. No data is moved by adding these settings.
To use host directories, edit these fields in the existing sections while retaining their other settings:
grafana:
data_directory: ./data/grafana
prometheus:
data_directory: ./data/prometheusRelative data paths are resolved against the checkout, including when the selected YAML is elsewhere. Absolute paths
also work. The containers keep their native paths: Grafana writes to /var/lib/grafana and Prometheus to /prometheus.
Create the host directories on the Docker host and make them writable by the respective container users before starting.
The utility never creates, changes ownership of, copies or deletes host data directories; missing bind directories make
startup fail instead of creating empty storage. check runs Prometheus validation, but does not test Grafana's storage
permissions or start Grafana. The provisioned dashboard is mounted under /etc/grafana/dashboards, outside the
Grafana data directory, so Docker does not create dashboard mount placeholders in your stored data.
Apply changes with power-monitor --config config.local.yaml check and power-monitor --config config.local.yaml up
(or omit --config for the default file). Storage selection is generated from YAML; no Compose override is needed.
Changing a directory selects a different store and does not migrate the previous data. Follow the
migration guide when retaining an existing installation.
reset --yes removes this project's managed Docker volumes but retains host directories and their contents.
.env.example keeps noncredential overrides commented out. YAML is sufficient for normal operation.
Compose resolves shell values ahead of .env; a nonempty resolved override replaces its corresponding YAML value.
An empty or unset override uses YAML. Remove or unset an old override when you want YAML edits to take effect.
| Environment override | YAML setting |
|---|---|
TAPO_PLUG_DEVICES |
exporter.exporters.tapo.devices |
PYPROM_EXPORTERS_LOG_LEVEL |
exporter.log_level |
GRAFANA_BIND_ADDRESS, GRAFANA_PORT |
grafana.bind_address, grafana.port |
GRAFANA_DATA_DIRECTORY |
grafana.data_directory |
PROMETHEUS_DATA_DIRECTORY |
prometheus.data_directory |
PROMETHEUS_RETENTION_TIME, PROMETHEUS_RETENTION_SIZE |
prometheus.retention_time, prometheus.retention_size |
PROMETHEUS_PORT |
exporter.prometheus_port |
PROMETHEUS_SCRAPE_INTERVAL |
prometheus.scrape_interval |
PROMETHEUS_SCRAPE_TIMEOUT |
prometheus.scrape_timeout |
PROMETHEUS_EVALUATION_INTERVAL |
prometheus.evaluation_interval |
A nonempty TAPO_PLUG_DEVICES replaces the entire YAML device list, with hosts separated by spaces or commas.
Whitespace/comma-only lists are invalid. Each YAML list item is one IP address or hostname; descriptions belong after #.
The effective configuration must contain at least one explicit host because broadcast discovery is disabled.
For example, clear any old device override in .env and run unset TAPO_PLUG_DEVICES in your shell before using YAML.
The defaults scrape the exporter every 30 seconds with a 25-second Prometheus timeout.
Under exporter.exporters.tapo:
prometheus_options.refresh_interval: nullprobes on scrape.prometheus_options.scrape_timeout: 20.0caps how long a scrape waits for the refresh.max_concurrent_devices: 10bounds concurrent device operations.discovery_options.timeout: 5sets the individual device request timeout in seconds. Both discovery timeouts use positive whole seconds, matching the exporter configuration schema.
Keep the exporter wait below Prometheus's scrape timeout, and the scrape timeout at or below the scrape interval.
A positive refresh_interval enables background polling instead of live probing. Adjust concurrency or polling intervals
based on observed device latency and LAN capacity; larger fleets and unreachable plugs can lengthen refreshes.
Overlapping scrapes share an in-flight refresh. If its wait expires, the exporter returns the latest available readings,
including completed healthy updates from that pass.
Validate and apply any changes using power-monitor check and power-monitor up.
With exporter 0.3.0 or newer, exporter.exporters.tapo.update_timeout optionally sets a positive, finite deadline in
seconds for one complete device update (default 10.0). It includes retries inside python-kasa; the exporter does not
repeat the whole update after that budget. A timed-out session gets up to one additional second for cleanup
(or update_timeout, if shorter) before recovery.
This differs from discovery_options.timeout, which limits individual device requests, and
prometheus_options.scrape_timeout, which limits how long an HTTP scrape waits. Do not add update_timeout when
using an older exporter image: its configuration schema does not support the option.
Healthy retained devices update before bounded retries of known failures and missing-host recovery. Successful device readings become available as their updates finish, so one slow or unavailable plug does not hold back every healthy reading. A failed plug loses its measurement series while retaining diagnostics and its last-success timestamp, then recovers through later refreshes. A scrape may finish while the shared refresh continues handling failures; check the operational panels for that distinction.
dash.json is a standalone dashboard that can also be imported into another Grafana installation.
Choose Dashboards → New → Import, upload the file, and select a Prometheus source using the dashboard's
Data source variable. The provisioned source has UID prometheus; the dashboard UID is tp-link-power.
Choose the job and exporter filters if your scrape labels differ. Source details stay in these filters instead of
repeating in device labels. Select a single job and exporter when different installations reuse device host addresses.
Compact legends sit below each graph, leaving its full width available for readings. Legend entries flow into multiple columns when space permits; Grafana's native legend does not enforce a fixed column count. Panels provide room for a twelve-device fleet without internal vertical scrolling. Use the Host and Device filters to focus larger selections. Hover a graph to see its device labels and values, or click a legend entry to focus a series.
Every device label uses alias · host, without a bracketed suffix. The host distinguishes plugs with identical aliases. Colors follow this name across graphs and ranked readings, even when ranking or filters change. Grafana's finite palette can reuse a color, so use the label to identify a device. The selected power total is a separate dashed, amber line.
Current-load and Wi-Fi panels retain labels beside each reading, including when only one device is selected. Wi-Fi rows put the weakest signal first; values closer to zero are stronger. Their colors identify devices, matching the graphs, rather than classifying signal quality.
The provisioned dashboard is managed by its JSON file. Save customizations back to dash.json, or make a separate
Grafana copy with a different UID. Provisioning updates the managed dashboard from disk; UI changes are disabled for it.
| Metric | Native unit | Dashboard meaning |
|---|---|---|
current_consumption |
W | Instantaneous device power |
current_voltage |
V | Device voltage, where supported |
current_current |
A | Device current, where supported |
current_rssi |
dBm | Received wireless signal strength, where supported |
current_consumption_today |
Wh | Energy accumulated on the plug's calendar day |
current_month_consumption |
Wh | Energy accumulated in the plug's calendar month |
tapo_discovered_devices |
Devices | Retained discovery inventory, not an online count |
Energy is already in Wh; Grafana scales its display to kWh when appropriate. Daily and monthly values are gauges that
reset on the plug's calendar. They are neither lifetime counters nor energy consumed during the dashboard's selected
range. The dashboard does not apply rate/increase to them or estimate energy from a fixed sampling cadence.
A blank reading means unavailable data, not zero. Totals include only selected devices that report the relevant metric. The same physical plug monitored by multiple exporters can be counted more than once. Model and firmware capabilities vary, so a working power reading does not imply that voltage, current, energy or Wi-Fi metrics are available.
Exporter scrape status and scrape age describe Prometheus requests, not device freshness. The operational panels require exporter 0.3.0 or newer and show each device's latest update result, update duration, time since its last successful SDK update, and failure/timeout rates. Never-successful devices have no last-success age. These diagnostics describe SDK communication; they do not establish physical-sensor freshness, relay state, or whether a plug will answer the next request.
Exporter-wide panels show the last completed refresh duration, whether a refresh is still running, and how often scrape callers exhaust their refresh wait. A successful HTTP scrape can return completed device updates while slower recovery work continues. Device diagnostics remain available for failed plugs; missing power readings are never replaced with zero. Graphs preserve gaps, and power queries hide device values when the exporter scrape has failed. Older exporters have no operational metrics; use their existing scrape panels and logs when investigating unchanged readings.
| Operational metric (exporter 0.3.0+) | Meaning |
|---|---|
tapo_device_update_success |
Latest SDK update succeeded: 1; failed or never succeeded: 0 |
tapo_device_last_success_timestamp_seconds |
Unix time of the last successful SDK update; 0 before success |
tapo_device_update_duration_seconds |
Latest complete SDK attempt, including internal retries; excludes queue/discovery/cleanup |
tapo_device_update_failures_total |
Failed SDK update attempts, including timeouts |
tapo_device_update_timeouts_total |
SDK update timeouts, including the whole-update deadline |
tapo_refresh_duration_seconds |
Duration of the last completed refresh pass |
tapo_refresh_in_progress |
Whether a shared refresh is running: 1 or 0 |
tapo_scrape_refresh_timeouts_total |
Scrape callers whose refresh wait expired |
Per-device diagnostics use host and alias; Prometheus adds job and instance. Failure and timeout panels display
counter rates, while duration/state panels display gauges. Failure counters include timeouts, so the two rates overlap.
Configured hosts that have never been discovered use alias unknown and success 0; discovery failures do not count as
SDK update attempts and therefore do not increment the update counters.
prometheus/alerts.yml evaluates five rules:
| Alert | Condition |
|---|---|
TapoExporterDown |
An exporter target fails scrapes for two minutes |
TapoNoPowerReadings |
A reachable exporter returns no power series for five minutes |
TapoPlugReadingsMissing |
A host seen within 24 hours has no current power series for five minutes |
TapoDeviceUpdateFailing |
A reachable exporter reports a failed or never-successful latest device update for five minutes |
TapoScrapeRefreshWaitsTimingOut |
At least three scrape-wait timeouts in five minutes, sustained for two minutes |
These rules are evaluated in Prometheus. No Alertmanager or notification delivery is configured. The missing-host rule cannot detect a plug that has never reported, or one absent for longer than its 24-hour history window. Intentionally removing a previously observed host can trigger it. The device-update and scrape-wait rules require exporter 0.3.0 or newer and a successful exporter scrape; the device-update rule also covers configured hosts that have never succeeded. These alerts do not establish physical-sensor freshness, and no universal last-success age threshold is applied. Use the age and refresh panels alongside the alerts when investigating stale readings.
For missing readings, check power-monitor status, then power-monitor logs exporter.
Confirm the YAML hosts and any active TAPO_PLUG_DEVICES override, reachability from Docker, credentials
and the device's energy-monitoring support.
If Grafana cannot query Prometheus, inspect power-monitor logs prometheus grafana and datasource provisioning.
uv sync --locked
uv run --locked prek install
uv run --locked pytest
uv run --locked prek run --all-filesPytest runs with up to four workers by default; use -n 0 for serial debugging.
For VS Code, run uv sync --locked and install the workspace extension recommendations.
The workspace defaults to .venv; if you already selected another interpreter, use Python: Select Interpreter
to choose this checkout's environment. Ruff and ty use the environment's tools and the project's configuration;
Markdown uses the shared markdownlint configuration, and JSON uses VS Code's built-in formatter.
Normal Test Explorer runs retain parallel execution. Debug Test uses the checked-in launch configuration
with -n 0 so breakpoints work in one process.
Editor terminals do not automatically import deployment .env values, avoiding stale shell overrides after file edits.
The CLI still loads .env; Python test/debug environment loading remains controlled separately by python.envFile.
Docker integration tests are skipped unless explicitly enabled. To run them against the pinned images:
make integration
# Equivalent:
RUN_STACK_INTEGRATION=1 uv run --locked pytest -n 0 -m integrationTo test a locally built exporter before publishing it, explicitly select its image for the disposable suite:
PYPROM_TEST_EXPORTER_IMAGE=pyprom-exporters:local-test make integrationThis override changes only copied test Compose files. Omitting it retains the image pinned in compose.yaml.
The integration suite uses an isolated Compose project and fake devices/metrics. It does not require physical plugs
or real account credentials. It validates configuration, provisioning, dashboard queries and monitoring failure cases.
make test runs the regular tests; make check runs all prek hooks.
Ruff enables all stable and preview rules, ty checks Python, and Markdown uses the same
markdownlint configuration as pyprom-exporters.
Python dependencies and tools are locked in uv.lock; update them with uv lock --upgrade, then rerun checks.
Keep audit notes in ignored scratch/ and generated reports in ignored report/.
The Code quality and tests GitHub Actions workflow runs on pushes to every branch and on pull requests. Lint/format/type checks, Python 3.11–3.14 tests and package builds, and Docker integration tests run as parallel jobs.
Dependabot checks weekly and combines Python dependencies (including transitive dependencies), GitHub Actions, Compose images and prek hooks into one version-update PR, with at most one such PR open at a time. Security updates are grouped separately for Python and GitHub Actions, the ecosystems here that support them. GitHub cannot combine security updates across ecosystems or with version updates, so security fixes may produce additional PRs. The version-update limit does not delay security updates. Dependabot configuration takes effect after it reaches the default branch.
compose.yaml: pinned Compose services, volumes and health checks.config/stack.yaml: all noncredential user settings, including device inventory.prometheus/: native configuration templates, alert rules and rule fixtures.grafana/provisioning/: native datasource and dashboard provisioning templates..runtime/: ignored generated native service configuration; do not edit it.dash.json: portable Grafana dashboard.src/grafana_tp_link/: thepower-monitorutility.tests/: configuration, dashboard, utility and isolated integration tests.docs/migration.md: preservation and upgrade guidance for an existing stack.
