feat(adb_command): selectable target, strict resolution, and a real response (t14)
The user's live run could not select his Shield for
nvidia_shield.adb_command, so he ran his diagnostics through the deprecated
androidtv integration instead. Measured against the installed HA (2026.10.1),
the cause is a contract, not a bug in the handler:
* ServiceRegistry.async_register has NO target parameter, so a target cannot be
declared at registration;
* the frontend renders the target picker from the service DESCRIPTION, which
HA builds from services.yaml or from the runtime async_set_service_schema —
and the runtime description WINS.
So async_setup now registers with supports_response=OPTIONAL and publishes
ADB_COMMAND_SERVICE_DESCRIPTION (target: {entity: [{domain: media_player}]},
fields, prose mirrored in strings.json/translations/en.json and pinned against
both by a test). No services.yaml is needed, and none is added.
The target resolution is strict, because the failure mode of guessing is a raw
shell command on the wrong device: exactly one PARTICIPANT, told apart as
no_target (nothing selected), unsupported_target (not a participant / option
off / another integration's entity) and ambiguous_target (two selected — sent
to neither). Accepts what HA actually sends: entity_id as text or as the LIST a
picked target produces, and device_id (an automation targeting the DEVICE),
expanded only through this integration's own registry entries and only to media
players. The output now comes back in the response under adb_response — the key
the user's captures read — while an unreachable Shield stays adb_unavailable,
an error rather than an empty response, and a call that does not ask for a
response is unchanged.
Evidence: 12 real-HA tests (new tests/test_adb_command_real_ha.py) drive the
real service registry, the real get_services websocket payload (the exact JSON
bytes the browser parses: target present, response optional, both fields) and
real async_call for every shape above. t14's CI gate globs tests/test_*real_ha.py
so a new real-HA file cannot skip silently; the skip/empty-collection guards stay
and were executed. The real-HA probe also caught async_entries_for_device being
a MODULE-LEVEL helper (not a registry method) that the stub had hidden, so the
drift gate's owner map now covers async_register, async_set_service_schema and
async_entries_for_device, with the call-argument floor raised 44 -> 52.
Verify: suites 681/4 and 682/3; real-HA 38 + the new file 12 (50 with the glob);
ruff clean; drift exit 0 (54 call arguments).
This commit is contained in:
1 parent
8a4e72b80a
commit
6aa3b6bfdd
13 files changed
+1087
-56
No files matched your search
@@ -162,14 +162,58 @@ the contractual path (YAML is the seed, the flow is the truth).
|
||||
### Raw `adb_command` service (opt-in)
|
||||
|
||||
When enabled in the integration's options (`adb_command_enabled`, **default
|
||||
off**), the integration registers the `nvidia_shield.adb_command` service:
|
||||
send a raw ADB shell command to the entry's persistent connection
|
||||
(`{"command": "your-command", "entity_id": "media_player.<yours>"}` — the
|
||||
entity is optional when exactly one entry provides the service). This is the
|
||||
same security posture as the Home Assistant core `androidtv` integration: a
|
||||
documented, opt-in escape hatch for debugging and automation, registered at
|
||||
setup and unregistered when the last participating entity unloads. It
|
||||
rejects (logs a warning, sends nothing) when the ADB connection is down.
|
||||
off**), the integration registers the `nvidia_shield.adb_command` service: send a
|
||||
raw ADB shell command to the entry's persistent connection.
|
||||
|
||||
**Picking the Shield in the UI.** The action declares a target, so **Developer
|
||||
tools → Actions** (and the automation editor) offers a target picker whose entity
|
||||
list contains the Shield's `media_player` entity — the same way the core
|
||||
`androidtv` integration's action worked. Select the Shield there, type the
|
||||
command, and call it.
|
||||
Home Assistant renders that picker from the action's *target description*, which
|
||||
the integration publishes at setup — that is why the Shield is selectable at all.
|
||||
|
||||
**As YAML / automation data**, the same call:
|
||||
|
||||
```yaml
|
||||
- action: nvidia_shield.adb_command
|
||||
target:
|
||||
entity_id: media_player.shield
|
||||
data:
|
||||
command: getprop ro.product.model
|
||||
response_variable: adb # optional: keep the output in a variable
|
||||
```
|
||||
|
||||
- `command` (**required**) — the shell command. An empty one is an error
|
||||
(`invalid_command`), never a silent no-op.
|
||||
- The target (**required**) is the Shield's `media_player` entity, or its
|
||||
`device_id` (`target: {device_id: …}` / `entity_id: …`). There is **no
|
||||
"pick the only one" fallback**: with no target the call fails with `no_target`,
|
||||
two targets fail with `ambiguous_target`, and an entity that does not participate
|
||||
(its entry has the option off) fails with `unsupported_target` — a raw shell
|
||||
command is never sent to a Shield you did not select.
|
||||
- **The output comes back** in the response under `adb_response` — the same key the
|
||||
deprecated `androidtv.adb_command` action returned, so a template written against
|
||||
that integration keeps reading the same field:
|
||||
|
||||
```yaml
|
||||
- action: nvidia_shield.adb_command
|
||||
target: {entity_id: media_player.shield}
|
||||
data: {command: "getprop ro.product.model"}
|
||||
response_variable: adb
|
||||
- action: notify.persistent_notification
|
||||
data:
|
||||
message: "{{ adb.adb_response }}"
|
||||
```
|
||||
|
||||
A caller that does not ask for a response is unchanged (the response is
|
||||
*optional*, not required). If ADB is down the action raises `adb_unavailable` —
|
||||
an error, never an empty response; a transport failure mid-command propagates as
|
||||
an error too.
|
||||
|
||||
This is the same security posture as the Home Assistant core `androidtv`
|
||||
integration: a documented, opt-in escape hatch for debugging and automation,
|
||||
registered at setup and unregistered when the last participating entity unloads.
|
||||
|
||||
### Screen capture (opt-in, manual)
|
||||
|
||||
|
||||
Reference in new issue
Block a user