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 useemeter, and ENE-capable strips useemeterwith the outlet argument forenergyor-o Nfor daily/monthly reports. KL135 / LB130 reportpower_mwandtotal_wh; KP115 reportsvoltage_mv,current_ma, andpower_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:
outletsshows the strip's outlet numbers, names, and on/off state.- Outlet numbers are
1-based and match the order shown byoutlets. - 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
ENEfeature flag. outlet-renamechanges the name shown byoutletsandinfo.
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
--klapor--tapoalias. - The alias registry parser accepts object entries with optional device identities, older object entries without identities, or legacy plain-string entries.
- If
hosts.jsonis 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, anddata.devicecontain device information, with local device identifiers removed. Device-specific fields retain their API names and depend on model/firmware.info --jsonno longer callsdoctor.doctor:datacontains the diagnostic report, includingerror_codewhen a device check fails. It validates response status, model, and explicit power state.energy: normalizeddata.measurement; unavailable measurements are null, not zero.power_w,voltage_v,current_a, andenergy_whuse W, V, A, and Wh.energy_whis the device-reported cumulative total; Tapo today/month totals are separate fields. These are not integrated estimates.energy-daily/energy-monthly:data.periodand sorteddata.entrieswithdayormonthandenergy_wh. An empty list is valid; a missing list is an error.group:data.resultscontains each alias's status, resulting power state, and error, plussucceeded/failedcounts. A partial failure still emits every result. Dry runs instead includedata.targetsanddry_run: true.scan:data.devicescontains each discovered/probed address's result, withfailedandregistry_updatedsummary fields.aliases:data.aliasesincludesidentity_knownwithout 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.