denki / docsGitHub ↗

Device support and commands

Supported devices

The table is generated from devices.toml. “Hardware verified” records existing hardware evidence, not a guarantee for every firmware or region. Unverified models have implementation support but still need real-device testing. HS300 verification covers the US model, hardware 2.0 / firmware 1.1.2.

Model Kind Protocol Hardware verified Features
KL135 Bulb kasa Yes power, dim, color_temp, color, energy, specs, presets
LB130 Bulb kasa No power, dim, color_temp, color, energy, specs, presets
KL430 LightStrip kasa No power, dim, color_temp, color, energy, effects
KL420L5 LightStrip kasa No power, dim, color_temp, color, energy, effects
HS220 Dimmer kasa No power, dim, schedules, led, clock
KP115 Plug kasa Yes power, energy, schedules, led, clock
HS110 Plug kasa Yes power, energy, schedules, led, clock
HS105 Plug kasa Yes power, schedules, led, clock
HS300 Strip kasa Yes power, energy, schedules, led, clock, outlets
KP303 Strip kasa No power, schedules, led, clock, outlets
P125 Tapo klap Yes power
P125 Tapo tapo (auto TPAP/KLAP) Partial; see notes below power
P110 Tapo klap No power, energy
P115 Tapo klap No power, energy
KP125M Tapo klap No power, energy
P125M Tapo klap No power

Tapo models use saved --klap aliases, or --tapo for the new P125 adapter. Power strips expose individual outlets; energy requires an ENE-capable model. Feature names in the table match the registry; color_temp means color temperature and dim means brightness.

Energy note: Bulbs and light strips use smartlife.iot.common.emeter; ENE-capable plugs use emeter, and ENE-capable strips use emeter with the outlet argument for energy or -o N for daily/monthly reports. KL135 / LB130 report power_mw and total_wh; KP115 reports voltage_mv, current_ma, and power_mw; HS110 reports real units (voltage, current, power).

Devices marked verified in devices.toml have been tested on real hardware.

Everyday commands

Discover devices

denki scan

scan reports Kasa observations without enrolling new aliases, and probes saved --klap and --tapo aliases. Enroll a Kasa device only after independently verifying its address with denki alias NAME IP. A reply-provided name is not approval. It stores the device identity locally. Existing v1/v2 registries remain readable; aliases without identity learn it at their current address on their next successful scan. It never moves an alias merely because a different address reports the same name.

Kasa UDP discovery is unauthenticated: anyone on the LAN can copy a device ID. If a reply claims a saved Kasa ID from a different IP, scan reports identity_rebind_required and leaves every existing alias unchanged. This also applies to legitimate DHCP moves. Verify the new IP independently (for example, in your router), then explicitly update each affected alias:

denki alias "desk lamp" 192.0.2.42

Replace the example IP with the verified address. A subsequent scan can learn its identity at that address. Kasa identity checks detect mismatches but do not provide cryptographic authentication; only use this legacy protocol on a trusted LAN.

Authenticated Tapo probes can still reconcile moved devices while preserving your chosen names. Tapo discovery requires known IPs: use denki scan --tapo-target 192.0.2.51 to probe a Tapo device and recover its existing alias by identity. The option can be repeated. An old, unreachable Tapo address is reported as a failed probe even if another probe finds the device at its new address. Saved identity mismatches stop commands before sending mutations.

Malformed UDP replies are ignored before deduplication, so a bad packet cannot hide a later valid response from the same address. A scan accepts at most 1,024 valid Kasa sources; exceeding that limit stops the scan without saving discoveries. Conflicting identities, conflicting saved protocols, and failed Tapo probes are reported individually; those partial scans return nonzero and retain successful results. Tapo probes are limited to four concurrent connections.

Kasa scan summaries do not recommend physical-control commands: a spoofed reply must not borrow the authority of a saved alias. Their JSON records include authenticated: false. info remains available when you explicitly choose a verified target. Command hints use POSIX shell quoting and are omitted for empty aliases, option-like aliases, or aliases containing terminal controls. Device names, firmware, effects, and errors are escaped in human-readable output. JSON escapes terminal-active Unicode while retaining original parsed values. CSV protects formula-leading string cells with an apostrophe; numeric measurements remain numeric. CSV printed directly to a terminal also escapes display controls. Use JSON/JSONL when exact original string values are required. Treat downloaded or piped CSV as data rather than replaying it into a terminal.

Inspect and control power

denki info "desk lamp"
denki on "desk lamp"
denki off "desk lamp"
denki toggle "desk lamp"
denki group off "office"
denki group off "office" --dry-run

Kasa group targets must have been explicitly enrolled with denki alias NAME IP. Older registry entries lack enrollment provenance, even if you originally added them by hand. Review denki aliases or a group --dry-run, verify each address independently, and re-save wanted Kasa aliases. A group containing any unapproved Kasa entry fails with alias_approval_required before contacting any device; it never silently skips the entry or partially runs the group. Read-only dry runs remain available and their JSON includes group_approved. Existing single-target commands remain compatible; review and remove unwanted legacy aliases too.

Use denki group <on|off|toggle> <pattern> when multiple aliases should be controlled together (for example denki group off "office" for Office Color 1, Office Color 2, Office Color 3). Group operations run concurrently (four devices at a time by default), continue when one device fails, and finish with a success/failure summary. Use --dry-run to review every matched alias without contacting devices, or --concurrency N to tune the limit.

Bulbs and dimmers

denki dim "desk lamp" 50
denki color-temp "desk lamp" 2700
denki color "desk lamp" -H 275 -s 50 -v 80
denki rainbow "desk lamp"

rainbow cycles a color bulb or light strip through hue steps while preserving its current saturation and brightness. The default delay is 1 second per step; use --step-delay-ms to change it (20–60000 ms). Press Ctrl-C to stop; the light stays at its current color.

To pulse a bulb's brightness while keeping it green, run a dim loop:

denki color "desk lamp" -H 120 -s 100 -v 100
while true; do
  for ((level = 0; level <= 100; level += 2)); do
    denki dim "desk lamp" "$level"
    sleep 0.2
  done
  for ((level = 98; level >= 0; level -= 2)); do
    denki dim "desk lamp" "$level"
    sleep 0.2
  done
done

This uses smaller 2% steps with a short hold at each level for a smoother pulse. Change the alias or sleep time to adjust the bulbs and pace. Press Ctrl-C to stop.

Light strip effects

denki effects "light strip"
denki effect "light strip" Aurora
denki effect "light strip" Off

effects lists built-in effect names and the active effect. effect activates one by name (for example Aurora, Rainbow, or Off).

KL420/KL430 strips also support the standard lighting commands:

denki on "light strip"
denki dim "light strip" 60
denki color-temp "light strip" 3000
denki color "light strip" -H 200 -s 80 -v 70

Energy

denki energy "desk plug"
denki energy-daily "desk plug" 2026-03
denki energy-monthly "desk plug" 2026

energy-daily defaults to the current month, and energy-monthly defaults to the current year. For KLAP-based Tapo energy plugs, energy shows current power plus today's and this month's totals. Tapo's local API does not expose the same daily/monthly history used by Kasa devices, so energy-daily and energy-monthly remain Kasa-only.

Automation building blocks

Commands return a non-zero exit status when a device cannot be reached or does not support an operation, making them suitable for scripts and scheduled jobs. Use friendly aliases to keep automation readable:

denki group off "office"
denki dim "bedroom lamp" 20
denki color-temp "bedroom lamp" 2700
denki effect "living room strip" Aurora

Every command accepts --json before or after the subcommand. Data goes to stdout; human-readable details and progress go to stderr. The group command supports --dry-run so an automation can verify its targets before changing device state. Missing or malformed success codes, invalid power state, rejected operations, and missing energy measurements return errors. Toggle never assumes unknown state means off.

Diagnostics and structured output

denki doctor "desk plug"
denki doctor "desk plug" --json
denki info "desk plug" --json

doctor shows Wi-Fi RSSI and actionable next steps, and checks alias resolution, protocol connectivity, device-info parsing, model support, firmware metadata, and advertised capabilities. Its versioned JSON report is suitable for bug reports and automation. It intentionally excludes raw device IDs, MAC addresses, Wi-Fi names, and credentials.

Power strips

denki outlets "power strip"
denki on "power strip" 2
denki off "power strip" 2
denki toggle "power strip" 2
denki energy "power strip" 2
denki energy-daily "power strip" 2026-03 -o 2
denki energy-monthly "power strip" 2026 -o 2
denki outlet-rename "power strip" 2 "Coffee Maker"

Notes:

  • outlets shows the strip's outlet numbers, names, and on/off state.
  • Outlet numbers are 1-based and match the order shown by outlets.
  • Omit the outlet number to target the whole strip; include it to target one outlet.
  • Per-outlet energy commands only work on strips with the ENE feature flag.
  • outlet-rename changes the name shown by outlets and info.

Device metadata

denki specs "desk lamp"
denki presets "desk lamp"
denki schedules "desk plug"
denki led "desk plug" on
denki clock "desk plug"
denki rename "desk plug" "Office Plug"
denki restart "desk plug"

Aliases and Tapo setup

Save a friendly name for a device

denki alias "floor lamp" 192.168.1.50

You must provide a valid IP address; invalid values are rejected immediately.

Aliasing is normalized for lookup, and duplicate aliases are detected using that same normalized form. For example, denki alias "Desk Lamp" ... will not both be allowed alongside an existing denki alias "desk lamp" ....

Use --klap for the original Tapo client:

denki alias "tapo plug" 192.168.1.51 --klap

For P125 plugs on recent firmware, use automatic TPAP/KLAP negotiation:

denki alias "P125 A" 192.0.2.10 --tapo
denki login "you@example.com"
denki info "P125 A"
denki info "P125 A" --json
denki on "P125 A"
denki off "P125 A"
denki toggle "P125 A"

--tpap is a synonym for --tapo. Both use the bundled, hardened tapo 0.12.2 code to negotiate TPAP or KLAP; neither forces TPAP. --klap keeps Denki's original KLAP client. The options are mutually exclusive. Saved protocol: "tapo" identifies the connection policy, not the negotiated wire protocol.

KLAP's handshake permits offline password guessing by someone who records it and knows or guesses the account email. Encryption and MAC checks do not repair this protocol property. Use a strong, unique Tapo password and a trusted or isolated LAN. Denki warns once per invocation before KLAP authentication. To require TPAP:

DENKI_REQUIRE_TPAP=1 denki info "P125 A"

This rejects direct KLAP and automatic KLAP/AES fallback before credentials or password-derived proofs are sent on those paths, including session refreshes. Protocol discovery may still contact the device without credentials. Unsupported or KLAP-only devices fail closed; the setting does not upgrade their firmware. Use 1/true to require TPAP and 0/false or unset for compatibility. The strict policy has offline regression coverage, not new physical-device evidence. It governs Tapo authentication only; it does not authenticate or disable Kasa commands.

On recent P125 firmware, Third-Party Compatibility on selects KLAP; off selects TPAP, as described in the upstream TPAP article. Denki does not change that app setting. The article explains why TPAP's SPAKE2+ login protects against offline password guessing; default compatibility mode does not provide that benefit when the device selects KLAP. Strict mode rejects that fallback. Other integrations may still need the compatibility setting on.

Supported today: P125 information, doctor, on/off/toggle, and grouped power commands. Writes check model, device type, saved identity, and resulting state. Toggle is read-then-write, so another controller can race it. Energy, outlets, lighting, schedules, timers, cameras, and hubs are not implemented by this adapter, even where upstream supports them. P125 has no energy capability in Denki's registry.

Hardware evidence: two P125 plugs, HW 1.0, FW 1.4.0 Build 260803 Rel.232153, passed information reads while advertising TPAP. One passed off → on → off with independent readbacks. This confirms reported relay state. The app setting was not independently confirmed; toggle, other firmware, and this adapter's KLAP path remain unverified. devices.toml tracks auto-mode evidence separately from original KLAP.

Re-save the same alias with --tapo to migrate it. At the same address, a known identity survives KLAP ↔ auto migration; other aliases are preserved. Use the alias or its IP for commands: saved addresses reuse their configured protocol. Auto-mode discovery probes saved addresses; scan --tapo-target IP uses an unambiguous saved protocol at that address; a new address uses automatic TPAP/KLAP negotiation. Repeated targets and aliases at the same address produce one Tapo probe. Conflicting saved protocols are rejected before discovery; a target saved as Kasa must be corrected explicitly first. Moved auto-mode devices can be reconciled by their authenticated identity. Existing explicit KLAP aliases retain KLAP; migrate those aliases explicitly before relying on auto-mode identity reconciliation.

Credentials use the saved login or TAPO_USER/TAPO_PASS. Stop on TPAP_CREDENTIALS or TPAP_AUTH_ATTEMPTS_LIMIT; repeated login attempts can lock the device. Denki adds no retry loop and bounds operations to 30 seconds. After a power timeout or readback failure, read info before retrying. For upstream failures, report model, hardware/firmware, app setting, and the sanitized error to upstream issues; never include credentials or raw captures. Denki routing/output failures belong in Denki's tracker.

Then use the alias anywhere you would use an IP address:

denki info "tapo plug"
denki on "tapo plug"

Notes:

  • Leading/trailing whitespace is trimmed when saving aliases.
  • Empty and all-whitespace alias names are rejected.
  • Alias names that normalize to an existing alias (case-insensitive, non-alphanumeric characters normalized to spaces, extra spaces collapsed) are rejected to avoid ambiguity in lookups.

Remove or list aliases

denki aliases
denki unalias "tapo plug"

Aliases are stored in hosts.json inside the platform's Denki configuration directory:

Platform Configuration directory
Linux $XDG_CONFIG_HOME/denki, or ~/.config/denki when unset
macOS ~/Library/Application Support/denki
Windows %APPDATA%\denki

denki aliases prints the actual registry path. These files are local to your user account; keep credentials and device identities out of bug reports.

Save Tapo credentials locally

export TAPO_USER="you@example.com"
export TAPO_PASS="your-tapo-password"

Or save them once:

denki login "you@example.com"

Passwords are no longer accepted as command-line arguments. The default hidden prompt avoids shell history and process-argument exposure. For automation, pipe a secret manager's output into denki login EMAIL --password-stdin, or redirect a private file. Do not write the literal password in an echo command or script. Stdin must be redirected and contain one non-empty UTF-8 line (maximum 4,096 bytes including its optional line ending); input and parse errors never echo it. Environment overrides remain supported, but may be exposed to other same-account processes or diagnostic tools. Prefer the hidden prompt for interactive use.

Tapo credentials are stored in credentials.json in the same configuration directory. Set both TAPO_USER and TAPO_PASS to override the saved file.

How device lookup works

  • Device names must be explicitly saved as aliases; scan reports untrusted names. Raw IP addresses are also accepted.
  • Exact normalized alias matches win first, then unambiguous normalized substring matches.
  • Raw IP addresses reuse the saved protocol when all aliases at that IP agree. Conflicting protocols are an error; unknown IPs retain the Kasa default. For a new Tapo IP, save an alias with --tapo (or --klap) first. An unreadable registry stops IP commands rather than guessing a protocol.
  • Tapo devices require a saved --klap or --tapo alias.
  • The alias registry parser accepts object entries with optional device identities, older object entries without identities, or legacy plain-string entries.
  • If hosts.json is malformed, the CLI prints parser details for both formats to make recovery easier.

Limitations

  • daily and monthly history for Tapo energy devices (the local API exposes current/day/month totals)
  • away mode (anti_theft) rule creation
  • countdown timer creation
  • schedule creation and deletion
  • firmware updates
  • strip-level energy monitoring for HS300/KP303 on non-ENE models

JSON result contract

Single-result commands emit exactly one JSON object with this envelope:

{"schema_version":2,"command":"energy","status":"ok","data":{"device":"desk plug","outlet":null,"source":"device","measurement":{"power_w":1.234,"voltage_v":null,"current_a":null,"energy_wh":null,"today_energy_wh":12.0,"month_energy_wh":345.0}},"error":null}

status is ok or error; the process exits nonzero on error. error is null or an object with stable code and human-readable message. Codes include invalid_arguments, not_found, malformed_response, device_rejected, unsupported_operation, identity_mismatch, identity_rebind_required, connection_failed, timeout, io_error, partial_failure, integrity_failed, registry_conflict, and the fallback command_failed. Messages may change; branch on codes. Resolution and argument failures also emit JSON. Help/version requests retain their normal output.

  • info: data.ip, data.protocol, and data.device contain device information, with local device identifiers removed. Device-specific fields retain their API names and depend on model/firmware. info --json no longer calls doctor.
  • doctor: data contains the diagnostic report, including error_code when a device check fails. It validates response status, model, and explicit power state.
  • energy: normalized data.measurement; unavailable measurements are null, not zero. power_w, voltage_v, current_a, and energy_wh use W, V, A, and Wh. energy_wh is the device-reported cumulative total; Tapo today/month totals are separate fields. These are not integrated estimates.
  • energy-daily / energy-monthly: data.period and sorted data.entries with day or month and energy_wh. An empty list is valid; a missing list is an error.
  • group: data.results contains each alias's status, resulting power state, and error, plus succeeded / failed counts. A partial failure still emits every result. Dry runs instead include data.targets and dry_run: true.
  • scan: data.devices contains each discovered/probed address's result, with failed and registry_updated summary fields.
  • aliases: data.aliases includes identity_known without exposing device IDs.
  • Other commands put their result under data; metadata APIs such as schedules, specs, and presets retain their device-specific namespace/method shape.

Migration: the former info --json / doctor --json output was a bare schema-v1 diagnostic report. Single-result output now uses the schema-v2 envelope. For doctor, move selectors such as .model to .data.model. Info now returns actual device details under .data.device instead of a diagnostic report.

Streaming energy measurements

denki energy watch "desk plug"
denki energy watch "desk plug" --interval 5 --count 120 --format jsonl > energy.jsonl
denki energy watch "power strip" --outlet 2 --interval 10 --format csv > energy.csv
denki energy watch "desk plug" --json --count 3

energy DEVICE [OUTLET] remains the one-shot command. watch is reserved as a subcommand in this position; use the device's IP if an alias is literally watch.

The default interval is five seconds, with a minimum of one second. Each completed sample is followed by that delay, so slow devices never cause overlapping requests or a catch-up burst. Omit --count to run until Ctrl-C; interruption cancels an in-flight read and exits cleanly. Polling only reads device information and energy; it never changes power. The Tapo authenticated session is reused, and a failed sample causes a reconnect on the next interval. Memory use is bounded; each sample is flushed immediately. Redirect stdout to a file to collect history locally.

JSONL emits one schema-v1 sample per line, with timestamp_unix_ms (UTC Unix epoch milliseconds at completion), device, outlet, status, source: "device", measurement, and error. --json is shorthand for JSONL here, not a final schema-v2 envelope; it cannot be combined with --format csv.

CSV columns are timestamp_unix_ms,device,outlet,status,power_w,voltage_v,current_a, energy_wh,today_energy_wh,month_energy_wh,error_code,error_message. Fields containing commas, quotes, or newlines are quoted. Missing values are empty CSV cells.

A failed sample has status: "error", null measurement in JSONL, and an error code/message. It never becomes a zero-watt sample. Later samples continue, but any failed sample makes the eventual process exit nonzero. Summaries go to stderr, so exported files contain only records. Historical collection starts when you run watch; it cannot reconstruct periods before collection or fill gaps while offline.

Safe local updates

Credential saves and alias changes use atomic file replacement. On Unix, credential files are created with mode 0600 before credentials are written. Concurrent Denki alias edits are serialized through an OS file lock. A scan that detects another process changed the registry returns registry_conflict and saves none of its updates; rerun denki scan to reconcile against the latest aliases.

The empty .lock sidecar files are intentionally retained. Do not remove them to unlock a process: locks are released automatically when the holding process exits. KLAP responses that fail authentication return integrity_failed before decryption or any state-dependent follow-up command.

Doctor JSON reports include next_steps alongside warnings and error_code. Weak Wi-Fi (RSSI below -70 dBm) adds advice without failing an otherwise successful check. An unreachable saved alias is not proof that a device still exists at its recorded IP.

Physical identification and fleet health

denki identify "desk plug" --confirm
denki identify "desk plug" --clear
denki doctor --all
denki doctor --all --concurrency 2 --timeout 20 --json
denki doctor --all --failed-only --json

identify requires the exact saved alias and records your own physical observation; it does not contact or switch the device. A saved alias, authentication, and model hardware verification are separate facts. Existing aliases start unconfirmed. Explicit address or protocol changes clear confirmation; authenticated discovery of the same identity preserves it. aliases shows the saved declaration.

Fleet diagnostics check each saved address once, preserve every alias in JSON, and skip conflicting protocol/identity assignments. Checks are read-only, use at most two concurrent probes by default (maximum eight), and have a 20-second deadline per address. Failed checks are not retried by the fleet runner. The JSON devices list contains aliases, diagnostics, and elapsed_ms; summary counts unique addresses, OK, failed, and warning results. Any failed address produces a nonzero exit status while retaining all results. Add --failed-only to show only failed addresses in text or JSON. This filters the displayed devices, not the checks: the summary and exit status still describe the whole fleet, and a healthy fleet shows no device rows. Failures include suggested next steps for unreachable devices, authentication problems, and conflicting aliases. An empty registry returns an empty successful summary. Progress is written to stderr as each address finishes, even with --json or --failed-only. Stdout retains the final report in address order. Each address's elapsed_ms measures its check, including a timeout, but excludes time waiting for a concurrency slot. Text reports show the same duration in milliseconds.

Confirmation shown by diagnostics is a historical user declaration, not proof that an offline or replaced device still matches its label.