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
@@ -147,11 +147,70 @@ APP_LABEL_GREP_SHAPES = "application-label|nonLocalizedLabel|labelRes"
|
||||
# found anywhere resets the run.
|
||||
APP_LABEL_PROBE_LIMIT = 5
|
||||
|
||||
# P4-2a adb_command service: rawADB shell on this entry — OFF by default
|
||||
# P4-2a adb_command service: raw ADB shell on this entry — OFF by default
|
||||
# (same security posture as the core androidtv integration).
|
||||
CONF_ADB_COMMAND_ENABLED = "adb_command_enabled"
|
||||
SERVICE_ADB_COMMAND = "adb_command"
|
||||
|
||||
#: t14: the key the command's output is returned under, in the service RESPONSE.
|
||||
#: It matches the deprecated ``androidtv.adb_command`` the user fell back to, so a
|
||||
#: template written against that integration keeps reading the same field.
|
||||
ADB_COMMAND_RESPONSE_KEY = "adb_response"
|
||||
|
||||
#: t14: the picker the UI offers for `nvidia_shield.adb_command`.
|
||||
#:
|
||||
#: MEASURED on HA 2026.10.1: ``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 instead (``services.yaml``, or a
|
||||
#: runtime ``async_set_service_schema`` call, which WINS over services.yaml) — this
|
||||
#: is that description's ``target``. Only the ENTITY kind is declared: the Shield is
|
||||
#: selected as its ``media_player`` entity (one per entry), exactly like the
|
||||
#: deprecated ``androidtv.adb_command`` the user used to fall back to.
|
||||
#:
|
||||
#: The entity domain the action's target declares, and the only domain a
|
||||
#: ``device_id`` expands to (a Shield device also carries sensors, a remote, an
|
||||
#: image entity — none of those is something to run a raw shell command on).
|
||||
ADB_COMMAND_TARGET_DOMAIN = "media_player"
|
||||
|
||||
ADB_COMMAND_SERVICE_TARGET: dict = {
|
||||
"entity": [{"domain": ADB_COMMAND_TARGET_DOMAIN}]
|
||||
}
|
||||
|
||||
#: t14: the service DESCRIPTION registered at setup (``async_set_service_schema``).
|
||||
#:
|
||||
#: Why a runtime description at all: the description is what the frontend receives
|
||||
#: from ``get_services``, and a service with no description has no ``target``, which
|
||||
#: is precisely why the user's UI offered no Shield to select and he had to run his
|
||||
#: diagnostics through the deprecated integration. The prose here mirrors
|
||||
#: ``strings.json``/``translations/en.json`` (where HA's translations live) so the
|
||||
#: form renders even before a translation is loaded; ``tests/test_adb_command_target.py``
|
||||
#: asserts the two cannot drift.
|
||||
ADB_COMMAND_SERVICE_DESCRIPTION: dict = {
|
||||
"name": "ADB command",
|
||||
"description": (
|
||||
"Send a raw ADB shell command to the persistent connection of an Nvidia "
|
||||
"Shield entry (option-gated use)."
|
||||
),
|
||||
"target": ADB_COMMAND_SERVICE_TARGET,
|
||||
"fields": {
|
||||
"command": {
|
||||
"name": "Command",
|
||||
"description": "The shell command to run on the device.",
|
||||
"required": True,
|
||||
"selector": {"text": {}},
|
||||
},
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
"description": (
|
||||
"The Shield to run it on: its media_player entity. Select it in "
|
||||
"the target picker above, or pass entity_id (or the Shield's "
|
||||
"device_id) in YAML."
|
||||
),
|
||||
"selector": {"entity": {"domain": "media_player"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# P5-3 (C-STREAM): live events push — the streaming_shell logcat consumer
|
||||
# (foreground AppState fast-path). Default OFF; the user flips it on and
|
||||
# reports stability on their device.
|
||||
|
||||
Reference in new issue
Block a user