feat(config-flow): menu of sub-screens on setup and options
Both configure pages were one long form: a field you wanted to change meant re-submitting everything, and the options page rendered the apps map as `[object Object]`. Verified against a real Home Assistant install (not assumed): the HTTP flow view serialises a field's suggested value verbatim, so a dict handed to a `type: string` field reaches the browser as a non-string; and real HA validates a submitted page against the schema the step returned, where the built-in EntitySelector rejects `''` and `None`. - The options flow is a MENU (Main setup / Receiver / Apps / Scripts / Save and close). Each sub-screen owns a disjoint set of keys and merges into one pending dict under a single rule: a key the submission CARRIES is authoritative (empty clears), a key it does not carry is untouched. The per-command script dict is rebuilt from the submitted script fields, so unsetting one persists instead of surviving as a stale key. - The sub-screen step_id IS the menu row id. Real HA dispatches a menu choice and a submitted form by that same name, so a mismatch is an UnknownStep or an unsubmittable page (the setup menu first tried a "setup_menu" step id and dead-ended in real HA). - The apps map reaches the suggestion layer serialized to the documented `package=name[|icon]` lines, and parses back to both maps. - The setup flow keeps its first form and gains a menu after it; pairing completion returns to the menu, and the "ADB then protocol" sequence still happens automatically. - Receiver accepts a `switch.*` as well as a media_player. Tests: 522 pass on stubs, and tests/test_config_flow_real_ha.py drives the real FlowManager + probatio + to_field_list; 53 + 11 of those fail on the pre-fix tree.
This commit is contained in:
1 parent
74e0549196
commit
cd8f528a27
3 files changed
+808
-206
No files matched your search
@@ -130,6 +130,49 @@ def _app_map_from_text(
|
||||
return app_map
|
||||
|
||||
|
||||
def _options_field_values(options: dict | None) -> dict:
|
||||
"""Entry options as FRONTEND-SERIALIZABLE field values (t56).
|
||||
|
||||
The bug this exists to kill: the apps map is stored as a DICT, and the
|
||||
options page handed that dict straight to ``add_suggested_values_to_schema``
|
||||
as the suggested value of a TEXT field. Real Home Assistant serializes the
|
||||
suggested value verbatim into the form the frontend receives (verified on a
|
||||
real install: ``{"suggested_value": {"ar.tvplayer.tv": "TiviMate"}}`` for a
|
||||
``type: string`` field), and the browser renders a non-string as
|
||||
``[object Object]``.
|
||||
|
||||
So the suggestion layer gets only what a text field can display: the apps
|
||||
map (plus its icon column) serialized to the documented
|
||||
``package=name[|icon]`` lines, and every other option value unchanged.
|
||||
"""
|
||||
options = dict(options or {})
|
||||
app_map = options.pop(CONF_APP_NAME_MAP, None)
|
||||
icon_map = options.pop(CONF_APP_ICON_MAP, None)
|
||||
if isinstance(app_map, dict):
|
||||
options[CONF_APP_NAME_MAP] = _app_map_to_text(
|
||||
app_map, icon_map if isinstance(icon_map, dict) else None
|
||||
)
|
||||
return options
|
||||
|
||||
|
||||
def _app_map_submission(value) -> tuple[dict, dict] | None:
|
||||
"""Parse the apps-table field into ``(app_map, icon_map)``; None if invalid.
|
||||
|
||||
A blank field is the documented way to clear the map; anything else must
|
||||
parse as ``package=name[|icon]`` lines or the whole submission is rejected
|
||||
(never half-stored).
|
||||
"""
|
||||
if value is None:
|
||||
return {}, {}
|
||||
if not str(value).strip():
|
||||
return {}, {}
|
||||
icons: dict[str, str] = {}
|
||||
app_map = _app_map_from_text(str(value), icons)
|
||||
if app_map is None:
|
||||
return None
|
||||
return app_map, icons
|
||||
|
||||
|
||||
def _host_field_key(field: str, host: str):
|
||||
"""The host-field KEY, carrying the known address as its default.
|
||||
|
||||
@@ -208,8 +251,13 @@ def _build_selectors():
|
||||
"media_player": _ClearingEntitySelector(
|
||||
selector.EntitySelectorConfig(domain="media_player")
|
||||
),
|
||||
# t56: a receiver is a media_player (an AVR/soundbar) OR a plain switch
|
||||
# (a soundbar/speaker that only knows on and off). The device_class
|
||||
# constraint belongs to the media_player half only, so a switch is not
|
||||
# filtered out by it — and the switch behaviour itself (turning it on
|
||||
# during playback) lives in the media_player task that follows.
|
||||
"receiver": _ClearingEntitySelector(
|
||||
selector.EntitySelectorConfig(domain="media_player", device_class="receiver")
|
||||
selector.EntitySelectorConfig(domain=["media_player", "switch"])
|
||||
),
|
||||
"power": _ClearingEntitySelector(
|
||||
selector.EntitySelectorConfig(domain=["switch", "binary_sensor"])
|
||||
@@ -289,14 +337,11 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
CONF_PROTOCOL_ENABLED: user_input.get(CONF_PROTOCOL_ENABLED, True),
|
||||
}
|
||||
|
||||
# If ADB is enabled, go to pairing step
|
||||
if self._user_data[CONF_ADB_ENABLED]:
|
||||
return await self.async_step_adb_pair()
|
||||
|
||||
if self._user_data[CONF_PROTOCOL_ENABLED]:
|
||||
return await self.async_step_protocol_pair()
|
||||
|
||||
return self._create_entry()
|
||||
# t56: the first form stays exactly one screen (so nothing it used
|
||||
# to accept moves or disappears), and the flow then becomes a MENU
|
||||
# of sub-screens — Connection / Receiver / the pairing steps /
|
||||
# Finish — instead of a single long page or a hidden sequence.
|
||||
return await self.async_step_menu()
|
||||
|
||||
return self._show_user_form(errors)
|
||||
|
||||
@@ -404,15 +449,19 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_user()
|
||||
|
||||
def _create_entry(self):
|
||||
"""Create the config entry from stored user data.
|
||||
"""Create the config entry from stored user data + setup sub-screens.
|
||||
|
||||
Everything the integration needs lives in the OPTIONS (t41: one host
|
||||
field among them); ``data`` stays empty because no value has to be
|
||||
immutable.
|
||||
immutable. The setup sub-screens (t56) contribute ``_setup_extras`` —
|
||||
disjoint keys merged over the first form, so a value configured on a
|
||||
sub-screen wins over the same key left blank on the first form.
|
||||
"""
|
||||
d = self._user_data
|
||||
d = {**self._user_data, **self._setup_extras}
|
||||
return self.async_create_entry(
|
||||
title=d["name"],
|
||||
# the name is the entry title; a flow driven programmatically (and
|
||||
# the first form always carries it) may omit it, so fall back
|
||||
title=d.get("name") or DEFAULT_NAME,
|
||||
data={},
|
||||
options={
|
||||
CONF_JELLYFIN_ENTITY: d.get(CONF_JELLYFIN_ENTITY, ""),
|
||||
@@ -428,11 +477,18 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
async def _async_after_pairing(self):
|
||||
"""Route after the pairing steps: protocol next, else create the entry."""
|
||||
if self._user_data.get(CONF_PROTOCOL_ENABLED):
|
||||
return await self.async_step_protocol_pair()
|
||||
return self._create_entry()
|
||||
"""Route after a pairing step: the protocol next, else the setup menu (t56).
|
||||
|
||||
The "ADB paired, now the protocol" sequence is deliberate and stays
|
||||
automatic — a user who asked for both transports should not have to find
|
||||
the second one in a menu — and the setup menu afterwards is where the
|
||||
remaining sub-screens (Connection / Receiver) and Finish live.
|
||||
"""
|
||||
if self._user_data.get(CONF_PROTOCOL_ENABLED) and not getattr(
|
||||
self, "_paired_protocol", False
|
||||
):
|
||||
return await self.async_step_protocol_pair()
|
||||
return self._show_setup_menu()
|
||||
|
||||
def _discard_protocol_client(self) -> None:
|
||||
"""Drop a pairing client whose session became void (host corrected)."""
|
||||
@@ -607,7 +663,11 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return self._finish_reauth()
|
||||
if self._reconfigure_entry is not None:
|
||||
return self._finish_reconfigure(mac)
|
||||
return self._create_entry()
|
||||
# t56: the protocol is paired now, so the setup menu must not offer to
|
||||
# pair it again — it returns to the menu (Receiver / Connection /
|
||||
# Finish) instead of creating the entry here.
|
||||
self._paired_protocol = True
|
||||
return self._show_setup_menu()
|
||||
|
||||
async def async_step_import(self, data):
|
||||
"""Seed an interactive flow from YAML (t30 U1, seed-only ruling).
|
||||
@@ -765,6 +825,157 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
or ""
|
||||
).strip()
|
||||
|
||||
# ── t56: the setup menu and its sub-screens ──
|
||||
|
||||
@property
|
||||
def _setup_extras(self) -> dict:
|
||||
"""Options collected on the setup sub-screens (t56), merged, not last-writer-wins.
|
||||
|
||||
Lazily created so the flow's ``__init__`` stays untouched; the menu's
|
||||
sub-screens each own a disjoint set of keys and merge into this dict, so
|
||||
configuring the receiver and then the connection (in either order)
|
||||
cannot drop the other's edits.
|
||||
"""
|
||||
if getattr(self, "_setup_extra_options", None) is None:
|
||||
self._setup_extra_options = {}
|
||||
return self._setup_extra_options
|
||||
|
||||
def _show_setup_menu(self, errors=None):
|
||||
"""The setup menu: connection, receiver, and finishing the entry (t56).
|
||||
|
||||
Home Assistant's standard menu step: it renders every option as a row
|
||||
and dispatches the choice to ``async_step_<option>``. Both transports
|
||||
stay optional (the pairing steps keep their skip), so the user can
|
||||
finish at any point. A transport that already paired is not offered
|
||||
again — that row would re-open a pairing session the Shield no longer
|
||||
needs.
|
||||
"""
|
||||
options = ["finish"]
|
||||
if self._user_data.get(CONF_ADB_ENABLED):
|
||||
options.insert(0, "adb")
|
||||
if self._user_data.get(CONF_PROTOCOL_ENABLED) and not getattr(
|
||||
self, "_paired_protocol", False
|
||||
):
|
||||
options.insert(0, "protocol")
|
||||
options.insert(0, "connection")
|
||||
options.insert(0, "receiver")
|
||||
return self.async_show_menu(
|
||||
step_id="menu", menu_options=options, errors=errors or {}
|
||||
)
|
||||
|
||||
async def async_step_menu(self, user_input=None):
|
||||
"""First visit: open the pending pairing; later visits: show the menu.
|
||||
|
||||
Deliberately NOT the menu's ``finish`` row (that dispatches to
|
||||
``async_step_finish``, a different method). The menu's own option ids
|
||||
are therefore ``['connection', 'receiver', 'adb'?, 'protocol'?,
|
||||
'finish']`` and never include ``menu`` itself, so the flow manager can
|
||||
never bounce back into this method — the one shape that would loop.
|
||||
|
||||
``user_input`` is never a menu choice here either: the flow manager
|
||||
reads ``next_step_id`` off the MENU result and calls the chosen step
|
||||
directly, then that step comes back here to re-render.
|
||||
"""
|
||||
if not getattr(self, "_pairing_underway", False):
|
||||
self._pairing_underway = True
|
||||
if self._user_data.get(CONF_ADB_ENABLED):
|
||||
return await self.async_step_adb_pair()
|
||||
if self._user_data.get(CONF_PROTOCOL_ENABLED):
|
||||
return await self.async_step_protocol_pair()
|
||||
return self._show_setup_menu()
|
||||
|
||||
def _finish_setup(self):
|
||||
"""Create the entry — the ``finish`` row and the no-transport path."""
|
||||
return self._create_entry()
|
||||
|
||||
def _apply_setup_submission(self, user_input: dict) -> None:
|
||||
"""Merge a sub-screen submission into the flow's pending options (t56).
|
||||
|
||||
ONE rule for every field, so "cleared" and "untouched" can never be
|
||||
confused: a key the submission CARRIES is authoritative — an empty
|
||||
value clears the setting — and a key it does NOT carry is untouched and
|
||||
keeps its stored value. The frontend submits every rendered field, so a
|
||||
missing key only ever means "that field was not on this screen".
|
||||
"""
|
||||
for key, value in user_input.items():
|
||||
if value is None:
|
||||
# an explicit null means the same as an empty value: clear
|
||||
self._setup_extras[key] = ""
|
||||
else:
|
||||
self._setup_extras[key] = value
|
||||
|
||||
async def async_step_receiver(self, user_input=None):
|
||||
"""Setup sub-screen: the receiver/soundbar this Shield plays through.
|
||||
|
||||
Accepts a media_player OR a plain switch (t56 string/selector change) —
|
||||
the switch behaviour itself lands with the media_player work that
|
||||
follows.
|
||||
"""
|
||||
if user_input is not None:
|
||||
self._apply_setup_submission(user_input)
|
||||
return await self.async_step_menu()
|
||||
|
||||
sel = _build_selectors()
|
||||
receiver = self._setup_extras.get(CONF_RECEIVER_ENTITY, "")
|
||||
fields: dict = {}
|
||||
if receiver:
|
||||
fields[vol.Optional(CONF_RECEIVER_ENTITY, default=receiver)] = sel["receiver"]
|
||||
else:
|
||||
fields[vol.Optional(CONF_RECEIVER_ENTITY)] = sel["receiver"]
|
||||
current_input = self._setup_extras.get(CONF_RECEIVER_INPUT, "")
|
||||
if current_input:
|
||||
fields[vol.Optional(CONF_RECEIVER_INPUT, default=current_input)] = str
|
||||
else:
|
||||
fields[vol.Optional(CONF_RECEIVER_INPUT)] = str
|
||||
return self.async_show_form(
|
||||
step_id="setup_receiver", data_schema=vol.Schema(fields)
|
||||
)
|
||||
|
||||
async def async_step_connection(self, user_input=None):
|
||||
"""Setup sub-screen: the address, the ADB port and the two transports.
|
||||
|
||||
The same three settings the first form carries, re-editable together —
|
||||
the first form is one screen by design (so nothing it used to accept can
|
||||
disappear), and this menu entry is the way back to the address after a
|
||||
pairing attempt failed. Merged like every other sub-screen.
|
||||
"""
|
||||
if user_input is not None:
|
||||
self._apply_setup_submission(user_input)
|
||||
if CONF_HOST in user_input:
|
||||
self._user_data[CONF_HOST] = str(user_input.get(CONF_HOST) or "").strip()
|
||||
if CONF_ADB_PORT in user_input:
|
||||
try:
|
||||
self._user_data[CONF_ADB_PORT] = int(user_input[CONF_ADB_PORT])
|
||||
except (TypeError, ValueError):
|
||||
self._user_data[CONF_ADB_PORT] = DEFAULT_ADB_PORT
|
||||
for key in (CONF_ADB_ENABLED, CONF_PROTOCOL_ENABLED):
|
||||
if key in user_input:
|
||||
self._user_data[key] = bool(user_input[key])
|
||||
return await self.async_step_menu()
|
||||
|
||||
host = self._host()
|
||||
port = self._user_data.get(CONF_ADB_PORT)
|
||||
if not isinstance(port, int):
|
||||
port = DEFAULT_ADB_PORT
|
||||
fields = {
|
||||
_host_field_key(CONF_HOST, host): str,
|
||||
vol.Optional(CONF_ADB_PORT, default=port): int,
|
||||
vol.Optional(
|
||||
CONF_ADB_ENABLED, default=bool(self._user_data.get(CONF_ADB_ENABLED, True))
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_PROTOCOL_ENABLED,
|
||||
default=bool(self._user_data.get(CONF_PROTOCOL_ENABLED, True)),
|
||||
): bool,
|
||||
}
|
||||
return self.async_show_form(
|
||||
step_id="setup_connection", data_schema=vol.Schema(fields)
|
||||
)
|
||||
|
||||
async def async_step_finish(self, user_input=None):
|
||||
"""The menu's finish row: create the entry from the first form + sub-screens."""
|
||||
return self._finish_setup()
|
||||
|
||||
def _show_adb_pair_form(self, errors=None):
|
||||
"""adb_pair form: the address/port (fixable here) and the skip escape.
|
||||
|
||||
@@ -805,7 +1016,12 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
# state when no key exists, and the Pair ADB button (only
|
||||
# registered for adb-enabled entries) pairs later without
|
||||
# re-adding the integration.
|
||||
return await self._async_after_pairing()
|
||||
#
|
||||
# t56: a skip means the ADB attempt is DECIDED, so the setup
|
||||
# menu takes over from here — the protocol is still offered on
|
||||
# it (unpaired), and the user reaches Connection/Receiver/Finish
|
||||
# without being marched through another form.
|
||||
return self._show_setup_menu()
|
||||
|
||||
typed_host = str(user_input.get(CONF_HOST) or "").strip()
|
||||
if typed_host:
|
||||
@@ -844,13 +1060,24 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
|
||||
class NvidiaShieldOptionsFlow(config_entries.OptionsFlowWithReload):
|
||||
"""Handle options for Nvidia Shield — single unified page (t29 G3b).
|
||||
"""Handle options for Nvidia Shield — a menu of sub-screens (t29 G3b, t56).
|
||||
|
||||
The constructor takes NO arguments: HA assigns ``self.config_entry``
|
||||
on instantiation (the pre-2026 "OptionsFlow(config_entry)" pattern is
|
||||
deprecated). Suggested values come through
|
||||
``add_suggested_values_to_schema``, and saving reloads via the
|
||||
OptionsFlowWithReload base — no listener boomerangs.
|
||||
The constructor takes NO arguments: HA assigns ``self.config_entry`` on
|
||||
instantiation (the pre-2026 ``OptionsFlow(config_entry)`` pattern is
|
||||
deprecated) and resolves it through ``hass.config_entries`` at call time.
|
||||
|
||||
The page is a standard Home Assistant MENU step: Home Assistant renders the
|
||||
rows, stores the choice as ``next_step_id``, and calls
|
||||
``async_step_<choice>(None)``. Every sub-screen merges into ONE pending
|
||||
options dict and returns to the menu, so an edit made on one sub-screen can
|
||||
never be lost by visiting another — the merge is key-scoped and
|
||||
deterministic, never last-writer-wins. "Save and close" commits it (and
|
||||
reloads through the OptionsFlowWithReload base).
|
||||
|
||||
Suggested values come through ``add_suggested_values_to_schema`` and are
|
||||
handed the SERIALIZED field values from ``_options_field_values``: the apps
|
||||
map is a dict in storage but must be the documented ``package=name[|icon]``
|
||||
text on the wire, or the browser renders ``[object Object]`` (t56).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
@@ -865,154 +1092,275 @@ class NvidiaShieldOptionsFlow(config_entries.OptionsFlowWithReload):
|
||||
self._options = {}
|
||||
return self._options
|
||||
|
||||
# ── the menu (t56) ──
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Single-page options."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
# t47 F1: a field submitted as None means "not supplied" — only an
|
||||
# explicit empty value ('' or a cleared selector) clears a setting, so
|
||||
# the two cannot be confused. Note the schema itself already tolerates
|
||||
# the empty value (see _ClearingEntitySelector).
|
||||
user_input = {key: value for key, value in user_input.items() if value is not None}
|
||||
# P5-2: validate the digit delay (0.05–1.0 s, §7 window)
|
||||
delay = user_input.pop(CONF_CHANNEL_DIGIT_DELAY, None)
|
||||
if delay is not None:
|
||||
try:
|
||||
delay = float(delay)
|
||||
except (TypeError, ValueError):
|
||||
errors["base"] = "invalid_digit_delay"
|
||||
delay = None
|
||||
if delay is not None and not (0.05 <= delay <= 1.0):
|
||||
errors["base"] = "invalid_digit_delay"
|
||||
# t44: one selector per command, stored as a single dict option so
|
||||
# the service layer reads one shape. Popped here because _options_dict
|
||||
# is updated with the rest of user_input verbatim below.
|
||||
scripts = {
|
||||
command: script
|
||||
for command in SCRIPT_COMMANDS
|
||||
if (
|
||||
script := str(
|
||||
user_input.pop(f"{SCRIPT_FIELD_PREFIX}{command}", "") or ""
|
||||
).strip()
|
||||
"""The options menu: Main setup / Apps / Scripts / Receiver / Save.
|
||||
|
||||
A MENU result never reaches this method with form data: the flow
|
||||
manager reads ``next_step_id`` off the menu result and calls the chosen
|
||||
step directly. Returning the menu here is what every sub-screen comes
|
||||
back to.
|
||||
"""
|
||||
return self.async_show_menu(
|
||||
step_id="init",
|
||||
menu_options=["connection", "receiver", "apps", "scripts", "finish"],
|
||||
)
|
||||
}
|
||||
# t47: one optional script for the remote entity — a plain string
|
||||
# option, unlike the per-command dict above
|
||||
remote_script = user_input.get(CONF_REMOTE_COMMAND_SCRIPT)
|
||||
if remote_script is not None:
|
||||
user_input[CONF_REMOTE_COMMAND_SCRIPT] = str(remote_script).strip()
|
||||
app_map_text = user_input.pop(CONF_APP_NAME_MAP, None)
|
||||
icons: dict[str, str] = {}
|
||||
if app_map_text is None:
|
||||
app_map = self._options_dict.get(CONF_APP_NAME_MAP, {})
|
||||
elif app_map_text.strip() == "":
|
||||
app_map = {}
|
||||
else:
|
||||
app_map = _app_map_from_text(app_map_text, icons)
|
||||
if app_map is None:
|
||||
errors["base"] = "invalid_app_map"
|
||||
if not errors:
|
||||
if delay is not None:
|
||||
self._options_dict[CONF_CHANNEL_DIGIT_DELAY] = delay
|
||||
self._options_dict.update(user_input)
|
||||
self._options_dict[CONF_APP_NAME_MAP] = app_map
|
||||
# P5-2: the icon column — the editor is the source of
|
||||
# truth, so a save without icons clears them (and a
|
||||
# single-column map stays valid).
|
||||
self._options_dict[CONF_APP_ICON_MAP] = icons
|
||||
# an empty dict clears every override (the editor is the truth)
|
||||
self._options_dict[CONF_COMMAND_SCRIPTS] = scripts
|
||||
|
||||
async def async_step_menu_again(self):
|
||||
"""Return to the options menu after a sub-screen saved (t56).
|
||||
|
||||
Deliberately NOT ``async_step_init``: HA dispatches a menu row to
|
||||
``async_step_<row id>`` by NAME, so ``async_step_init`` is not what the
|
||||
menu returns to — and calling it here would also bypass the ``step_id``
|
||||
the browser expects on the next render.
|
||||
"""
|
||||
return await self.async_step_init()
|
||||
|
||||
async def async_step_finish(self, user_input=None):
|
||||
"""Save and close: commit the merged options (the base reloads)."""
|
||||
return self.async_create_entry(title="", data=self._options_dict)
|
||||
|
||||
# ── the ONE merge rule ──
|
||||
|
||||
def _apply_submission(self, user_input: dict, errors: dict) -> bool:
|
||||
"""Merge a sub-screen submission into the pending options.
|
||||
|
||||
The rule, so "cleared" and "untouched" can never be confused:
|
||||
|
||||
* a key the submission CARRIES is authoritative — an empty value (``''``
|
||||
from a cleared selector, ``None`` from an explicit null, a whitespace
|
||||
string) CLEARS the setting, a value sets it;
|
||||
* a key the submission does NOT carry is untouched and keeps its stored
|
||||
value.
|
||||
|
||||
Real Home Assistant submits every field the step rendered, so a missing
|
||||
key only ever means "that field was not on this screen" (verified with a
|
||||
real FlowManager: the submitted ``user_input`` is exactly the coerced
|
||||
schema output for the rendered fields). The per-command scripts are the
|
||||
one nested-shaped option, so when ANY script field is part of a
|
||||
submission the whole dict is rebuilt from the submitted fields: that is
|
||||
what makes unsetting a script persist instead of surviving as a stale
|
||||
key, and it keeps ``command_scripts`` in the shape the service layer
|
||||
reads (flat ``command -> script``).
|
||||
|
||||
Returns True when the submission is valid and merged.
|
||||
"""
|
||||
present = {
|
||||
key: value
|
||||
for key, value in user_input.items()
|
||||
if key in _ALL_OPTION_FIELDS
|
||||
}
|
||||
|
||||
# ── the apps table: parse or reject, never half-store ──
|
||||
if CONF_APP_NAME_MAP in present:
|
||||
parsed = _app_map_submission(present[CONF_APP_NAME_MAP])
|
||||
if parsed is None:
|
||||
errors["base"] = "invalid_app_map"
|
||||
return False
|
||||
app_map, icon_map = parsed
|
||||
present[CONF_APP_NAME_MAP] = app_map
|
||||
present[CONF_APP_ICON_MAP] = icon_map
|
||||
|
||||
# ── the channel digit delay (§7 window) ──
|
||||
if CONF_CHANNEL_DIGIT_DELAY in present:
|
||||
try:
|
||||
delay = float(present[CONF_CHANNEL_DIGIT_DELAY])
|
||||
except (TypeError, ValueError):
|
||||
errors["base"] = "invalid_digit_delay"
|
||||
return False
|
||||
if not 0.05 <= delay <= 1.0:
|
||||
errors["base"] = "invalid_digit_delay"
|
||||
return False
|
||||
present[CONF_CHANNEL_DIGIT_DELAY] = delay
|
||||
|
||||
# ── the per-command scripts: the submitted fields ARE the dict ──
|
||||
#
|
||||
# Rebuilding from the submitted fields is what makes removal persist: a
|
||||
# script configured for `turn_on` and then cleared arrives as '' for
|
||||
# that field, and a dict built only from the non-empty fields drops it
|
||||
# instead of leaving a stale key behind. A command the submission does
|
||||
# not carry is therefore cleared too — the Scripts screen renders every
|
||||
# field, so the only way a key goes missing is a hand-made partial
|
||||
# submission, and this keeps the old "a save without scripts clears
|
||||
# them" contract unambiguous rather than shape-dependent.
|
||||
if any(field.startswith(SCRIPT_FIELD_PREFIX) for field in present):
|
||||
present[CONF_COMMAND_SCRIPTS] = {
|
||||
command: str(present.get(f"{SCRIPT_FIELD_PREFIX}{command}") or "").strip()
|
||||
for command in SCRIPT_COMMANDS
|
||||
if str(present.get(f"{SCRIPT_FIELD_PREFIX}{command}") or "").strip()
|
||||
}
|
||||
for field in [f for f in present if f.startswith(SCRIPT_FIELD_PREFIX)]:
|
||||
# UI only: the service layer reads the dict, never a flat key.
|
||||
# Dropped AFTER the rebuild — the rebuild reads them.
|
||||
present.pop(field)
|
||||
|
||||
for key, value in present.items():
|
||||
if value is None:
|
||||
value = ""
|
||||
if isinstance(value, str):
|
||||
value = value.strip()
|
||||
self._options_dict[key] = value
|
||||
return True
|
||||
|
||||
# ── sub-screens ──
|
||||
|
||||
async def async_step_connection(self, user_input=None):
|
||||
"""Main setup: the link, both transports and the linked entities."""
|
||||
errors: dict = {}
|
||||
if user_input is not None and self._apply_submission(dict(user_input), errors):
|
||||
return await self.async_step_menu_again()
|
||||
return self._show_connection_form(errors)
|
||||
|
||||
def _show_connection_form(self, errors=None):
|
||||
"""Main setup: the same settings the first setup form carries.
|
||||
|
||||
Deliberately its own field list rather than ``_user_schema_dict``: that
|
||||
builder renders the entry's NAME (a config-entry title, not an option —
|
||||
it would be a field whose edits are silently dropped) and it takes no
|
||||
logcat/adb-command toggles. The field NAMES stay identical, which is
|
||||
what a sub-screen has to preserve.
|
||||
"""
|
||||
sel = _build_selectors()
|
||||
schema_dict = {}
|
||||
options = self._options_dict
|
||||
port = options.get(CONF_ADB_PORT)
|
||||
schema_dict = {
|
||||
_host_field_key(CONF_HOST, str(options.get(CONF_HOST) or "")): str,
|
||||
vol.Optional(
|
||||
CONF_ADB_PORT, default=port if isinstance(port, int) else DEFAULT_ADB_PORT
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_ADB_ENABLED, default=bool(options.get(CONF_ADB_ENABLED, True))
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_ADB_STORE_APP_INFO,
|
||||
default=bool(options.get(CONF_ADB_STORE_APP_INFO, True)),
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_PROTOCOL_ENABLED,
|
||||
default=bool(options.get(CONF_PROTOCOL_ENABLED, True)),
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_LOGCAT_EVENTS, default=bool(options.get(CONF_LOGCAT_EVENTS, False))
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_ADB_COMMAND_ENABLED,
|
||||
default=bool(options.get(CONF_ADB_COMMAND_ENABLED, False)),
|
||||
): bool,
|
||||
vol.Optional(CONF_JELLYFIN_ENTITY): sel["media_player"],
|
||||
vol.Optional(CONF_POWER_ENTITY): sel["power"],
|
||||
}
|
||||
return self._show_sub_form("connection", schema_dict, errors)
|
||||
|
||||
# Apps name/icon map editor (P4-1, core androidtv_remote options
|
||||
# pattern, simplified): one package=name[|icon] per line (P5-2 adds
|
||||
# the optional icon column).
|
||||
schema_dict[vol.Optional(CONF_APP_NAME_MAP)] = str
|
||||
async def async_step_receiver(self, user_input=None):
|
||||
"""Receiver: the media_player OR switch this Shield plays through."""
|
||||
errors: dict = {}
|
||||
if user_input is not None and self._apply_submission(dict(user_input), errors):
|
||||
return await self.async_step_menu_again()
|
||||
return self._show_receiver_form(errors)
|
||||
|
||||
# Jellyfin
|
||||
jf = self._options_dict.get(CONF_JELLYFIN_ENTITY)
|
||||
jf_key = vol.Optional(CONF_JELLYFIN_ENTITY)
|
||||
if jf:
|
||||
jf_key = vol.Optional(CONF_JELLYFIN_ENTITY)
|
||||
schema_dict[jf_key] = sel["media_player"]
|
||||
|
||||
# Receiver
|
||||
recv = self._options_dict.get(CONF_RECEIVER_ENTITY)
|
||||
recv_key = vol.Optional(CONF_RECEIVER_ENTITY)
|
||||
if recv:
|
||||
recv_key = vol.Optional(CONF_RECEIVER_ENTITY)
|
||||
schema_dict[recv_key] = sel["receiver"]
|
||||
|
||||
# t44: command script overrides — the escape hatch for hardware we do not
|
||||
# drive (a speaker switch owning the volume). Unset = native path as before.
|
||||
schema_dict[vol.Optional(CONF_REMOTE_COMMAND_SCRIPT)] = sel["script"]
|
||||
|
||||
configured_scripts = command_scripts(self._options_dict)
|
||||
suggested_scripts = {}
|
||||
for command in SCRIPT_COMMANDS:
|
||||
field = f"{SCRIPT_FIELD_PREFIX}{command}"
|
||||
schema_dict[vol.Optional(field)] = sel["script"]
|
||||
if configured_scripts.get(command):
|
||||
suggested_scripts[field] = configured_scripts[command]
|
||||
|
||||
# P5-2: channel digit behavior (the user-validated §7 flow)
|
||||
schema_dict[vol.Optional(CONF_CHANNEL_DIGIT_DELAY)] = str
|
||||
schema_dict[vol.Optional(CONF_CHANNEL_SEND_ENTER)] = bool
|
||||
|
||||
# P5-3: the live events push is opt-in (default off; the user
|
||||
# tests stability on their device and reports)
|
||||
schema_dict[vol.Optional(CONF_LOGCAT_EVENTS)] = bool
|
||||
|
||||
# P4-2a: the raw adb_command service is opt-in (default off)
|
||||
schema_dict[vol.Optional(CONF_ADB_COMMAND_ENABLED)] = bool
|
||||
|
||||
# Receiver input — dropdown from DAG if receiver is set, text otherwise
|
||||
current_input = self._options_dict.get(CONF_RECEIVER_INPUT, "")
|
||||
receiver = self._options_dict.get(CONF_RECEIVER_ENTITY)
|
||||
inputs = _get_receiver_inputs(self.hass, receiver)
|
||||
|
||||
if inputs:
|
||||
input_sel = selector.SelectSelector(
|
||||
def _show_receiver_form(self, errors=None):
|
||||
sel = _build_selectors()
|
||||
# Receiver input: a dropdown from the DAG input map when the linked
|
||||
# entity publishes one, a plain text field otherwise — a plain switch
|
||||
# never publishes one, so it gets the text field.
|
||||
inputs = _get_receiver_inputs(
|
||||
self.hass, self._options_dict.get(CONF_RECEIVER_ENTITY)
|
||||
)
|
||||
input_field = (
|
||||
selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=inputs,
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
custom_value=True,
|
||||
)
|
||||
)
|
||||
input_key = vol.Optional(CONF_RECEIVER_INPUT)
|
||||
if current_input:
|
||||
input_key = vol.Optional(CONF_RECEIVER_INPUT)
|
||||
schema_dict[input_key] = input_sel
|
||||
else:
|
||||
schema_dict[vol.Optional(CONF_RECEIVER_INPUT)] = str
|
||||
|
||||
# Power
|
||||
pwr = self._options_dict.get(CONF_POWER_ENTITY)
|
||||
pwr_key = vol.Optional(CONF_POWER_ENTITY)
|
||||
if pwr:
|
||||
pwr_key = vol.Optional(CONF_POWER_ENTITY)
|
||||
schema_dict[pwr_key] = sel["power"]
|
||||
|
||||
# Transport toggles (t41: both transports are ours; the skip paths and
|
||||
# this page are the way to turn either one off)
|
||||
schema_dict[vol.Optional(CONF_ADB_ENABLED)] = bool
|
||||
schema_dict[vol.Optional(CONF_PROTOCOL_ENABLED)] = bool
|
||||
schema_dict[vol.Optional(CONF_ADB_STORE_APP_INFO)] = bool
|
||||
|
||||
# The ONE address field (t41), plus the ADB port
|
||||
schema_dict[vol.Optional(CONF_HOST)] = str
|
||||
schema_dict[vol.Optional(CONF_ADB_PORT)] = int
|
||||
|
||||
# t29 G3b: suggested values come from the entry options via the
|
||||
# documented helper — no per-key `default=` in the schema build.
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(schema_dict),
|
||||
{**self._options_dict, **suggested_scripts},
|
||||
),
|
||||
errors=errors,
|
||||
if inputs
|
||||
else str
|
||||
)
|
||||
schema_dict = {
|
||||
vol.Optional(CONF_RECEIVER_ENTITY): sel["receiver"],
|
||||
vol.Optional(CONF_RECEIVER_INPUT): input_field,
|
||||
}
|
||||
return self._show_sub_form("receiver", schema_dict, errors)
|
||||
|
||||
async def async_step_apps(self, user_input=None):
|
||||
"""Apps: the editable apps table — one row per app, blank when empty."""
|
||||
errors: dict = {}
|
||||
if user_input is not None and self._apply_submission(dict(user_input), errors):
|
||||
return await self.async_step_menu_again()
|
||||
return self._show_apps_form(errors)
|
||||
|
||||
def _show_apps_form(self, errors=None):
|
||||
schema_dict = {
|
||||
# One ROW per line, exactly the stored shape and the documented
|
||||
# editor format: `package=name` with an optional `|icon`. The field
|
||||
# is pre-filled with the serialized map (never the dict), and the
|
||||
# empty state is simply blank — no discovery data exists yet in this
|
||||
# task, so there are no rows to invent and nothing to crash on.
|
||||
vol.Optional(CONF_APP_NAME_MAP): str,
|
||||
}
|
||||
return self._show_sub_form("apps", schema_dict, errors)
|
||||
|
||||
async def async_step_scripts(self, user_input=None):
|
||||
"""Scripts: per-command overrides and the channel digit behaviour."""
|
||||
errors: dict = {}
|
||||
if user_input is not None and self._apply_submission(dict(user_input), errors):
|
||||
return await self.async_step_menu_again()
|
||||
return self._show_scripts_form(errors)
|
||||
|
||||
def _show_scripts_form(self, errors=None):
|
||||
sel = _build_selectors()
|
||||
schema_dict = {vol.Optional(CONF_REMOTE_COMMAND_SCRIPT): sel["script"]}
|
||||
for command in SCRIPT_COMMANDS:
|
||||
schema_dict[vol.Optional(f"{SCRIPT_FIELD_PREFIX}{command}")] = sel["script"]
|
||||
schema_dict[vol.Optional(CONF_CHANNEL_DIGIT_DELAY)] = str
|
||||
schema_dict[vol.Optional(CONF_CHANNEL_SEND_ENTER)] = bool
|
||||
return self._show_sub_form("scripts", schema_dict, errors)
|
||||
|
||||
# ── rendering ──
|
||||
|
||||
def _show_sub_form(self, step_id: str, schema_dict: dict, errors: dict | None):
|
||||
"""Render a sub-screen with only ITS suggested values — never a dict.
|
||||
|
||||
``_options_field_values`` is what serializes the apps map for the wire,
|
||||
and the script selectors are suggested from the normalized
|
||||
``command_scripts`` dict — the same accessor the service layer reads, so
|
||||
the page cannot offer a script the runtime would ignore.
|
||||
"""
|
||||
suggested = _options_field_values(self._options_dict)
|
||||
for command, script in command_scripts(self._options_dict).items():
|
||||
suggested[f"{SCRIPT_FIELD_PREFIX}{command}"] = script
|
||||
return self.async_show_form(
|
||||
step_id=step_id,
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(schema_dict), suggested
|
||||
),
|
||||
errors=errors or {},
|
||||
)
|
||||
|
||||
|
||||
#: Every field the options page can render. A submission key outside this set is
|
||||
#: menu/HA bookkeeping (`next_step_id`) and must never reach the stored options.
|
||||
_ALL_OPTION_FIELDS = frozenset(
|
||||
{
|
||||
CONF_HOST,
|
||||
CONF_ADB_PORT,
|
||||
CONF_ADB_ENABLED,
|
||||
CONF_PROTOCOL_ENABLED,
|
||||
CONF_ADB_STORE_APP_INFO,
|
||||
CONF_LOGCAT_EVENTS,
|
||||
CONF_ADB_COMMAND_ENABLED,
|
||||
CONF_JELLYFIN_ENTITY,
|
||||
CONF_POWER_ENTITY,
|
||||
CONF_RECEIVER_ENTITY,
|
||||
CONF_RECEIVER_INPUT,
|
||||
CONF_APP_NAME_MAP,
|
||||
CONF_APP_ICON_MAP,
|
||||
CONF_REMOTE_COMMAND_SCRIPT,
|
||||
CONF_CHANNEL_DIGIT_DELAY,
|
||||
CONF_CHANNEL_SEND_ENTER,
|
||||
CONF_COMMAND_SCRIPTS,
|
||||
*{f"{SCRIPT_FIELD_PREFIX}{command}" for command in SCRIPT_COMMANDS},
|
||||
}
|
||||
)
|
||||
@@ -3,7 +3,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "NVIDIA Shield",
|
||||
"description": "{discovery_note}Set up your NVIDIA Shield. Everything here is optional except one state source \u2014 the Nvidia Remote Protocol or ADB. Link the devices and services you want to use.",
|
||||
"description": "{discovery_note}Set up your NVIDIA Shield. Everything here is optional except one state source — the Nvidia Remote Protocol or ADB. Link the devices and services you want to use.",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"jellyfin_entity": "Jellyfin media player",
|
||||
@@ -18,33 +18,33 @@
|
||||
},
|
||||
"data_description": {
|
||||
"jellyfin_entity": "The Jellyfin media_player for this Shield. Leave blank to detect it automatically.",
|
||||
"receiver_entity": "The receiver or soundbar this Shield plays through.",
|
||||
"receiver_entity": "The receiver, soundbar or speaker switch this Shield plays through. Leave blank if there is none.",
|
||||
"receiver_input": "The receiver input the Shield uses, for example 'Shield'. Leave blank if you are not sure.",
|
||||
"power_entity": "A smart plug switch, if you want reliable on/off detection.",
|
||||
"adb_enabled": "Reads now-playing information over ADB from any app. Requires Developer options > Network debugging on the Shield.",
|
||||
"adb_store_app_info": "Shows the title and artist from non-Jellyfin apps. Turn off to show only the app name (privacy mode).",
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it only if you need a different address.",
|
||||
"adb_port": "The ADB port. The standard value is 5555 \u2014 change it only if ADB uses a custom port on your network.",
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered — change it only if you need a different address.",
|
||||
"adb_port": "The ADB port. The standard value is 5555 — change it only if ADB uses a custom port on your network.",
|
||||
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). It needs a one-time PIN pairing right after this step."
|
||||
}
|
||||
},
|
||||
"adb_pair": {
|
||||
"title": "Allow ADB access",
|
||||
"description": "On the Shield, open Settings > System > Developer options and turn on Network debugging (it may be called ADB debugging). Then select Submit \u2014 the Shield shows an Allow prompt; approve it. Nothing happened? Check the address below and select Submit again. Not now? Turn on 'Continue without ADB pairing' and select Submit.",
|
||||
"description": "On the Shield, open Settings > System > Developer options and turn on Network debugging (it may be called ADB debugging). Then select Submit — the Shield shows an Allow prompt; approve it. Nothing happened? Check the address below and select Submit again. Not now? Turn on 'Continue without ADB pairing' and select Submit.",
|
||||
"data": {
|
||||
"host": "The Shield's IP address",
|
||||
"adb_port": "ADB port",
|
||||
"skip_pairing": "Continue without ADB pairing"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it only if the Shield moved or you gave it a different address.",
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered — change it only if the Shield moved or you gave it a different address.",
|
||||
"adb_port": "The standard ADB port is 5555.",
|
||||
"skip_pairing": "Finishes setup now and leaves ADB unpaired. The Pair ADB button stays available on the device page, so you can pair later."
|
||||
}
|
||||
},
|
||||
"protocol_pair": {
|
||||
"title": "Pair the remote protocol",
|
||||
"description": "While this step is open the Shield shows a pairing code. Type it below and select Submit. If no code appears on the Shield, check the IP address below and select Submit again \u2014 pairing restarts and the Shield shows a new code. To set up without the remote protocol, turn on 'Continue without protocol pairing'.",
|
||||
"description": "While this step is open the Shield shows a pairing code. Type it below and select Submit. If no code appears on the Shield, check the IP address below and select Submit again — pairing restarts and the Shield shows a new code. To set up without the remote protocol, turn on 'Continue without protocol pairing'.",
|
||||
"data": {
|
||||
"pairing_code": "Pairing code shown on the Shield",
|
||||
"host": "The Shield's IP address",
|
||||
@@ -52,13 +52,64 @@
|
||||
},
|
||||
"data_description": {
|
||||
"pairing_code": "The code the Shield displays while this step is open.",
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it if the Shield moved.",
|
||||
"skip_pairing": "Finishes setup with the remote protocol off \u2014 it stays off until you pair it. Pair later from this integration's entry menu: choose Reconfigure."
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered — change it if the Shield moved.",
|
||||
"skip_pairing": "Finishes setup with the remote protocol off — it stays off until you pair it. Pair later from this integration's entry menu: choose Reconfigure."
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "Re-pair Nvidia Shield",
|
||||
"description": "The Shield no longer trusts the saved pairing certificate, usually after a factory reset. Select Submit to pair again \u2014 the Shield then shows a code."
|
||||
"description": "The Shield no longer trusts the saved pairing certificate, usually after a factory reset. Select Submit to pair again — the Shield then shows a code."
|
||||
},
|
||||
"menu": {
|
||||
"title": "Finish setup",
|
||||
"description": "Pair the transports and check anything else before the entry is created.",
|
||||
"menu": {
|
||||
"receiver": "Receiver",
|
||||
"connection": "Connection",
|
||||
"adb": "Set up ADB",
|
||||
"protocol": "Pair the remote protocol",
|
||||
"finish": "Create the entry"
|
||||
},
|
||||
"data": {
|
||||
"host": "The Shield's IP address",
|
||||
"adb_port": "ADB port",
|
||||
"adb_enabled": "Read state over ADB",
|
||||
"protocol_enabled": "Add the Nvidia Remote Protocol"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered — change it only if you need a different address.",
|
||||
"adb_port": "The ADB port. The standard value is 5555 — change it only if ADB uses a custom port on your network.",
|
||||
"adb_enabled": "Reads now-playing information over ADB from any app. Requires Developer options > Network debugging on the Shield.",
|
||||
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). It needs a one-time PIN pairing right after this step."
|
||||
}
|
||||
},
|
||||
"setup_connection": {
|
||||
"title": "Connection",
|
||||
"description": "The Shield's address, the ADB port and which transports this entry uses.",
|
||||
"data": {
|
||||
"host": "The Shield's IP address",
|
||||
"adb_port": "ADB port",
|
||||
"adb_enabled": "Read state over ADB",
|
||||
"protocol_enabled": "Add the Nvidia Remote Protocol"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered — change it only if you need a different address.",
|
||||
"adb_port": "The ADB port. The standard value is 5555 — change it only if ADB uses a custom port on your network.",
|
||||
"adb_enabled": "Reads now-playing information over ADB from any app. Requires Developer options > Network debugging on the Shield.",
|
||||
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). It needs a one-time PIN pairing right after this step."
|
||||
}
|
||||
},
|
||||
"setup_receiver": {
|
||||
"title": "Receiver",
|
||||
"description": "The receiver or soundbar this Shield plays through — a media_player, or a plain switch.",
|
||||
"data": {
|
||||
"receiver_entity": "Receiver",
|
||||
"receiver_input": "Receiver input"
|
||||
},
|
||||
"data_description": {
|
||||
"receiver_entity": "The receiver, soundbar or speaker switch this Shield plays through. Leave blank if there is none.",
|
||||
"receiver_input": "The receiver input the Shield uses, for example 'Shield'. Leave blank if you are not sure."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -67,9 +118,9 @@
|
||||
"protocol_no_host": "No IP address for the Shield yet. Fill in the host field above (a discovered Shield is pre-filled automatically).",
|
||||
"protocol_pair_failed": "The Shield rejected that pairing code. Check the code on the Shield and select Submit to try again.",
|
||||
"protocol_unavailable": "The androidtvremote2 library is not installed, so the remote protocol cannot pair. Install it and restart Home Assistant, or turn on 'Continue without protocol pairing' to finish setup anyway.",
|
||||
"protocol_unreachable": "The Shield did not answer at that address. Check the address above and that the Shield is awake, then select Submit to try again \u2014 or turn on 'Continue without protocol pairing' to finish setup without it.",
|
||||
"protocol_unreachable": "The Shield did not answer at that address. Check the address above and that the Shield is awake, then select Submit to try again — or turn on 'Continue without protocol pairing' to finish setup without it.",
|
||||
"no_state_source": "No state source selected. Choose at least one: 'Add the Nvidia Remote Protocol' or 'Read state over ADB'.",
|
||||
"invalid_app_map": "Each line must be package=name with a non-empty package (no spaces) and a non-empty name \u2014 for example ar.tvplayer.tv=TiviMate.",
|
||||
"invalid_app_map": "Each line must be package=name with a non-empty package (no spaces) and a non-empty name — for example ar.tvplayer.tv=TiviMate.",
|
||||
"invalid_digit_delay": "The digit delay must be between 0.05 and 1.0 seconds."
|
||||
},
|
||||
"abort": {
|
||||
@@ -77,7 +128,7 @@
|
||||
"reauth_successful": "Re-paired successfully.",
|
||||
"reauth_skipped": "Pairing skipped. The saved certificate is still not trusted, so the remote protocol stays unavailable until you pair again.",
|
||||
"reauth_entry_missing": "This entry no longer exists.",
|
||||
"reconfigure_skipped": "Nothing changed \u2014 the remote protocol stays off until you pair it. Choose Reconfigure from this integration's entry menu when you are ready.",
|
||||
"reconfigure_skipped": "Nothing changed — the remote protocol stays off until you pair it. Choose Reconfigure from this integration's entry menu when you are ready.",
|
||||
"reconfigure_entry_missing": "This entry no longer exists, so there is nothing to reconfigure.",
|
||||
"zeroconf_no_host": "The discovery event carried no usable address.",
|
||||
"yaml_seed_only": {},
|
||||
@@ -92,6 +143,13 @@
|
||||
"init": {
|
||||
"title": "NVIDIA Shield settings",
|
||||
"description": "Settings for this Shield. Saving reloads the integration so the changes take effect.",
|
||||
"menu": {
|
||||
"connection": "Main setup",
|
||||
"receiver": "Receiver",
|
||||
"apps": "Apps",
|
||||
"scripts": "Scripts",
|
||||
"finish": "Save and close"
|
||||
},
|
||||
"data": {
|
||||
"jellyfin_entity": "Jellyfin media player",
|
||||
"receiver_entity": "Receiver",
|
||||
@@ -117,22 +175,91 @@
|
||||
"remote_command_script": "Remote command script"
|
||||
},
|
||||
"data_description": {
|
||||
"jellyfin_entity": "The Jellyfin media_player for this Shield. Stream details come from the HA Jellyfin integration \u2014 no extra setup needed.",
|
||||
"jellyfin_entity": "The Jellyfin media_player for this Shield. Stream details come from the HA Jellyfin integration — no extra setup needed.",
|
||||
"receiver_entity": "The receiver or soundbar this Shield plays through.",
|
||||
"receiver_input": "The receiver input the Shield uses, for example 'Shield'.",
|
||||
"power_entity": "A smart plug switch, if you want reliable on/off detection.",
|
||||
"adb_enabled": "Reads now-playing information over ADB from any app. Requires Developer options > Network debugging on the Shield.",
|
||||
"adb_store_app_info": "Shows the title and artist from non-Jellyfin apps. Turn off to show only the app name (privacy mode).",
|
||||
"host": "The Shield's IP address. Change it only if the Shield moved or you gave it a different address.",
|
||||
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). Turning it on needs a one-time PIN pairing \u2014 use Reconfigure on the entry to pair.",
|
||||
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). Turning it on needs a one-time PIN pairing — use Reconfigure on the entry to pair.",
|
||||
"adb_port": "The ADB port. The standard value is 5555.",
|
||||
"app_name_map": "One entry per line, package=name (for example ar.tvplayer.tv=TiviMate), with an optional |icon after the name. Leave blank to clear.",
|
||||
"adb_command_enabled": "Adds the nvidia_shield.adb_command service (a raw ADB shell command over the persistent connection). Security-relevant: leave it off unless you need it \u2014 the same opt-in posture as the core androidtv integration.",
|
||||
"channel_digit_delay": "Pause between the channel digits entered via play_media(channel) \u2014 0.05 to 1.0 seconds (default 0.1, the tested value).",
|
||||
"adb_command_enabled": "Adds the nvidia_shield.adb_command service (a raw ADB shell command over the persistent connection). Security-relevant: leave it off unless you need it — the same opt-in posture as the core androidtv integration.",
|
||||
"channel_digit_delay": "Pause between the channel digits entered via play_media(channel) — 0.05 to 1.0 seconds (default 0.1, the tested value).",
|
||||
"channel_send_enter": "Presses ENTER after the last digit (protocol key first, then the ADB keyevent fallback). Off by default, matching TiviMate's tested behavior.",
|
||||
"logcat_events": "Detects app changes instantly through a dedicated logcat stream (AdbDevice.streaming_shell). Off by default \u2014 polling keeps the state correct either way, and the integration falls back to polling if the stream keeps failing.",
|
||||
"logcat_events": "Detects app changes instantly through a dedicated logcat stream (AdbDevice.streaming_shell). Off by default — polling keeps the state correct either way, and the integration falls back to polling if the stream keeps failing.",
|
||||
"remote_command_script": "One script for remote.send_command. It replaces the key path when set and receives the call's own parameters as variables: command (the list of key commands), num_repeats, delay_secs and hold_secs."
|
||||
}
|
||||
},
|
||||
"connection": {
|
||||
"title": "Main setup",
|
||||
"description": "The Shield's address, the transports it is read over, and the devices this entry links to. Changes here are kept when you save.",
|
||||
"data": {
|
||||
"host": "The Shield's IP address",
|
||||
"adb_port": "ADB port",
|
||||
"adb_enabled": "Read state over ADB",
|
||||
"adb_store_app_info": "Show title and artist",
|
||||
"protocol_enabled": "Add the Nvidia Remote Protocol",
|
||||
"logcat_events": "Watch app launches over logcat",
|
||||
"adb_command_enabled": "Allow the adb_command action",
|
||||
"jellyfin_entity": "Jellyfin media player",
|
||||
"power_entity": "Power switch"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The Shield's IP address. Change it only if the Shield moved.",
|
||||
"adb_port": "The ADB port. The standard value is 5555 — change it only if ADB uses a custom port on your network.",
|
||||
"adb_enabled": "Reads now-playing information over ADB from any app. Requires Developer options > Network debugging on the Shield.",
|
||||
"adb_store_app_info": "Shows the title and artist from non-Jellyfin apps. Turn off to show only the app name (privacy mode).",
|
||||
"protocol_enabled": "The all-in-one remote-protocol transport (power, availability, app push and volume readout). It needs the one-time PIN pairing from setup.",
|
||||
"logcat_events": "Uses a second ADB stream to notice app launches the moment they happen. Leave off unless you want the faster app switch.",
|
||||
"adb_command_enabled": "Exposes the adb_command action for this entry's media player. Off by default; it runs arbitrary shell commands on the Shield.",
|
||||
"jellyfin_entity": "The Jellyfin media_player for this Shield. Leave blank to detect it automatically.",
|
||||
"power_entity": "A smart plug switch, if you want reliable on/off detection."
|
||||
}
|
||||
},
|
||||
"receiver": {
|
||||
"title": "Receiver",
|
||||
"description": "The receiver or soundbar this Shield plays through — a media_player, or a plain switch for a soundbar that only turns on and off.",
|
||||
"data": {
|
||||
"receiver_entity": "Receiver",
|
||||
"receiver_input": "Receiver input"
|
||||
},
|
||||
"data_description": {
|
||||
"receiver_entity": "The receiver, soundbar or speaker switch this Shield plays through. Leave blank if there is none.",
|
||||
"receiver_input": "The receiver input the Shield uses, for example 'Shield'. Leave blank if you are not sure."
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"title": "Apps",
|
||||
"description": "One app per line: the package id, an equals sign, the name to show, and optionally a pipe and an icon (a picture URL or a camera entity id). Blank uses the built-in list.",
|
||||
"data": {
|
||||
"app_name_map": "App names and icons"
|
||||
},
|
||||
"data_description": {
|
||||
"app_name_map": "One row per app, for example ar.tvplayer.tv=TiviMate|http://192.168.1.10/tivi.png. The name is what the source list, the Current App sensor and the media player show."
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"title": "Scripts",
|
||||
"description": "Optional: run a Home Assistant script instead of the built-in command, per command. Clear a row to go back to the built-in path.",
|
||||
"data": {
|
||||
"remote_command_script": "Script for remote.send_command",
|
||||
"script_turn_on": "turn_on script",
|
||||
"script_turn_off": "turn_off script",
|
||||
"script_volume_up": "volume_up script",
|
||||
"script_volume_down": "volume_down script",
|
||||
"script_volume_set": "volume_set script",
|
||||
"script_volume_mute": "volume_mute script",
|
||||
"script_select_source": "select_source script",
|
||||
"channel_digit_delay": "Delay between channel digits (seconds)",
|
||||
"channel_send_enter": "Send ENTER after channel digits"
|
||||
},
|
||||
"data_description": {
|
||||
"remote_command_script": "Replaces the whole remote.send_command path for this Shield. Leave blank to use the built-in key presses.",
|
||||
"channel_digit_delay": "The gap between the digits of a channel number, 0.05 to 1.0 seconds. Default 0.1.",
|
||||
"channel_send_enter": "Turn on if your channel app needs ENTER after the digits."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -155,19 +282,19 @@
|
||||
"exceptions": {
|
||||
"invalid_command": {
|
||||
"title": "Empty command",
|
||||
"message": "The command field is empty \u2014 nothing was sent."
|
||||
"message": "The command field is empty — nothing was sent."
|
||||
},
|
||||
"unsupported_target": {
|
||||
"title": "Unsupported entity for adb_command",
|
||||
"message": "{entity_id} is not an adb_command participant (its entry must enable the adb_command_enabled option) \u2014 command not sent."
|
||||
"message": "{entity_id} is not an adb_command participant (its entry must enable the adb_command_enabled option) — command not sent."
|
||||
},
|
||||
"adb_unavailable": {
|
||||
"title": "ADB unavailable",
|
||||
"message": "The ADB connection of {entity_id} is down \u2014 command not sent."
|
||||
"message": "The ADB connection of {entity_id} is down — command not sent."
|
||||
},
|
||||
"unknown_source": {
|
||||
"title": "Unknown source",
|
||||
"message": "'{source}' is not one of this Shield's apps. Use a name from the source list \u2014 the app names from the Apps map, or the built-in list when no map is set."
|
||||
"message": "'{source}' is not one of this Shield's apps. Use a name from the source list — the app names from the Apps map, or the built-in list when no map is set."
|
||||
},
|
||||
"command_script_failed": {
|
||||
"title": "Command script failed",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "NVIDIA Shield",
|
||||
"description": "{discovery_note}Set up your NVIDIA Shield. Everything here is optional except one state source \u2014 the Nvidia Remote Protocol or ADB. Link the devices and services you want to use.",
|
||||
"description": "{discovery_note}Set up your NVIDIA Shield. Everything here is optional except one state source — the Nvidia Remote Protocol or ADB. Link the devices and services you want to use.",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"jellyfin_entity": "Jellyfin media player",
|
||||
@@ -18,33 +18,33 @@
|
||||
},
|
||||
"data_description": {
|
||||
"jellyfin_entity": "The Jellyfin media_player for this Shield. Leave blank to detect it automatically.",
|
||||
"receiver_entity": "The receiver or soundbar this Shield plays through.",
|
||||
"receiver_entity": "The receiver, soundbar or speaker switch this Shield plays through. Leave blank if there is none.",
|
||||
"receiver_input": "The receiver input the Shield uses, for example 'Shield'. Leave blank if you are not sure.",
|
||||
"power_entity": "A smart plug switch, if you want reliable on/off detection.",
|
||||
"adb_enabled": "Reads now-playing information over ADB from any app. Requires Developer options > Network debugging on the Shield.",
|
||||
"adb_store_app_info": "Shows the title and artist from non-Jellyfin apps. Turn off to show only the app name (privacy mode).",
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it only if you need a different address.",
|
||||
"adb_port": "The ADB port. The standard value is 5555 \u2014 change it only if ADB uses a custom port on your network.",
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered — change it only if you need a different address.",
|
||||
"adb_port": "The ADB port. The standard value is 5555 — change it only if ADB uses a custom port on your network.",
|
||||
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). It needs a one-time PIN pairing right after this step."
|
||||
}
|
||||
},
|
||||
"adb_pair": {
|
||||
"title": "Allow ADB access",
|
||||
"description": "On the Shield, open Settings > System > Developer options and turn on Network debugging (it may be called ADB debugging). Then select Submit \u2014 the Shield shows an Allow prompt; approve it. Nothing happened? Check the address below and select Submit again. Not now? Turn on 'Continue without ADB pairing' and select Submit.",
|
||||
"description": "On the Shield, open Settings > System > Developer options and turn on Network debugging (it may be called ADB debugging). Then select Submit — the Shield shows an Allow prompt; approve it. Nothing happened? Check the address below and select Submit again. Not now? Turn on 'Continue without ADB pairing' and select Submit.",
|
||||
"data": {
|
||||
"host": "The Shield's IP address",
|
||||
"adb_port": "ADB port",
|
||||
"skip_pairing": "Continue without ADB pairing"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it only if the Shield moved or you gave it a different address.",
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered — change it only if the Shield moved or you gave it a different address.",
|
||||
"adb_port": "The standard ADB port is 5555.",
|
||||
"skip_pairing": "Finishes setup now and leaves ADB unpaired. The Pair ADB button stays available on the device page, so you can pair later."
|
||||
}
|
||||
},
|
||||
"protocol_pair": {
|
||||
"title": "Pair the remote protocol",
|
||||
"description": "While this step is open the Shield shows a pairing code. Type it below and select Submit. If no code appears on the Shield, check the IP address below and select Submit again \u2014 pairing restarts and the Shield shows a new code. To set up without the remote protocol, turn on 'Continue without protocol pairing'.",
|
||||
"description": "While this step is open the Shield shows a pairing code. Type it below and select Submit. If no code appears on the Shield, check the IP address below and select Submit again — pairing restarts and the Shield shows a new code. To set up without the remote protocol, turn on 'Continue without protocol pairing'.",
|
||||
"data": {
|
||||
"pairing_code": "Pairing code shown on the Shield",
|
||||
"host": "The Shield's IP address",
|
||||
@@ -52,13 +52,64 @@
|
||||
},
|
||||
"data_description": {
|
||||
"pairing_code": "The code the Shield displays while this step is open.",
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered \u2014 change it if the Shield moved.",
|
||||
"skip_pairing": "Finishes setup with the remote protocol off \u2014 it stays off until you pair it. Pair later from this integration's entry menu: choose Reconfigure."
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered — change it if the Shield moved.",
|
||||
"skip_pairing": "Finishes setup with the remote protocol off — it stays off until you pair it. Pair later from this integration's entry menu: choose Reconfigure."
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "Re-pair Nvidia Shield",
|
||||
"description": "The Shield no longer trusts the saved pairing certificate, usually after a factory reset. Select Submit to pair again \u2014 the Shield then shows a code."
|
||||
"description": "The Shield no longer trusts the saved pairing certificate, usually after a factory reset. Select Submit to pair again — the Shield then shows a code."
|
||||
},
|
||||
"menu": {
|
||||
"title": "Finish setup",
|
||||
"description": "Pair the transports and check anything else before the entry is created.",
|
||||
"menu": {
|
||||
"receiver": "Receiver",
|
||||
"connection": "Connection",
|
||||
"adb": "Set up ADB",
|
||||
"protocol": "Pair the remote protocol",
|
||||
"finish": "Create the entry"
|
||||
},
|
||||
"data": {
|
||||
"host": "The Shield's IP address",
|
||||
"adb_port": "ADB port",
|
||||
"adb_enabled": "Read state over ADB",
|
||||
"protocol_enabled": "Add the Nvidia Remote Protocol"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered — change it only if you need a different address.",
|
||||
"adb_port": "The ADB port. The standard value is 5555 — change it only if ADB uses a custom port on your network.",
|
||||
"adb_enabled": "Reads now-playing information over ADB from any app. Requires Developer options > Network debugging on the Shield.",
|
||||
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). It needs a one-time PIN pairing right after this step."
|
||||
}
|
||||
},
|
||||
"setup_connection": {
|
||||
"title": "Connection",
|
||||
"description": "The Shield's address, the ADB port and which transports this entry uses.",
|
||||
"data": {
|
||||
"host": "The Shield's IP address",
|
||||
"adb_port": "ADB port",
|
||||
"adb_enabled": "Read state over ADB",
|
||||
"protocol_enabled": "Add the Nvidia Remote Protocol"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The Shield's IP address. Pre-filled when the Shield was discovered — change it only if you need a different address.",
|
||||
"adb_port": "The ADB port. The standard value is 5555 — change it only if ADB uses a custom port on your network.",
|
||||
"adb_enabled": "Reads now-playing information over ADB from any app. Requires Developer options > Network debugging on the Shield.",
|
||||
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). It needs a one-time PIN pairing right after this step."
|
||||
}
|
||||
},
|
||||
"setup_receiver": {
|
||||
"title": "Receiver",
|
||||
"description": "The receiver or soundbar this Shield plays through — a media_player, or a plain switch.",
|
||||
"data": {
|
||||
"receiver_entity": "Receiver",
|
||||
"receiver_input": "Receiver input"
|
||||
},
|
||||
"data_description": {
|
||||
"receiver_entity": "The receiver, soundbar or speaker switch this Shield plays through. Leave blank if there is none.",
|
||||
"receiver_input": "The receiver input the Shield uses, for example 'Shield'. Leave blank if you are not sure."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -67,9 +118,9 @@
|
||||
"protocol_no_host": "No IP address for the Shield yet. Fill in the host field above (a discovered Shield is pre-filled automatically).",
|
||||
"protocol_pair_failed": "The Shield rejected that pairing code. Check the code on the Shield and select Submit to try again.",
|
||||
"protocol_unavailable": "The androidtvremote2 library is not installed, so the remote protocol cannot pair. Install it and restart Home Assistant, or turn on 'Continue without protocol pairing' to finish setup anyway.",
|
||||
"protocol_unreachable": "The Shield did not answer at that address. Check the address above and that the Shield is awake, then select Submit to try again \u2014 or turn on 'Continue without protocol pairing' to finish setup without it.",
|
||||
"protocol_unreachable": "The Shield did not answer at that address. Check the address above and that the Shield is awake, then select Submit to try again — or turn on 'Continue without protocol pairing' to finish setup without it.",
|
||||
"no_state_source": "No state source selected. Choose at least one: 'Add the Nvidia Remote Protocol' or 'Read state over ADB'.",
|
||||
"invalid_app_map": "Each line must be package=name with a non-empty package (no spaces) and a non-empty name \u2014 for example ar.tvplayer.tv=TiviMate.",
|
||||
"invalid_app_map": "Each line must be package=name with a non-empty package (no spaces) and a non-empty name — for example ar.tvplayer.tv=TiviMate.",
|
||||
"invalid_digit_delay": "The digit delay must be between 0.05 and 1.0 seconds."
|
||||
},
|
||||
"abort": {
|
||||
@@ -77,7 +128,7 @@
|
||||
"reauth_successful": "Re-paired successfully.",
|
||||
"reauth_skipped": "Pairing skipped. The saved certificate is still not trusted, so the remote protocol stays unavailable until you pair again.",
|
||||
"reauth_entry_missing": "This entry no longer exists.",
|
||||
"reconfigure_skipped": "Nothing changed \u2014 the remote protocol stays off until you pair it. Choose Reconfigure from this integration's entry menu when you are ready.",
|
||||
"reconfigure_skipped": "Nothing changed — the remote protocol stays off until you pair it. Choose Reconfigure from this integration's entry menu when you are ready.",
|
||||
"reconfigure_entry_missing": "This entry no longer exists, so there is nothing to reconfigure.",
|
||||
"zeroconf_no_host": "The discovery event carried no usable address.",
|
||||
"yaml_seed_only": {},
|
||||
@@ -92,6 +143,13 @@
|
||||
"init": {
|
||||
"title": "NVIDIA Shield settings",
|
||||
"description": "Settings for this Shield. Saving reloads the integration so the changes take effect.",
|
||||
"menu": {
|
||||
"connection": "Main setup",
|
||||
"receiver": "Receiver",
|
||||
"apps": "Apps",
|
||||
"scripts": "Scripts",
|
||||
"finish": "Save and close"
|
||||
},
|
||||
"data": {
|
||||
"jellyfin_entity": "Jellyfin media player",
|
||||
"receiver_entity": "Receiver",
|
||||
@@ -117,22 +175,91 @@
|
||||
"remote_command_script": "Remote command script"
|
||||
},
|
||||
"data_description": {
|
||||
"jellyfin_entity": "The Jellyfin media_player for this Shield. Stream details come from the HA Jellyfin integration \u2014 no extra setup needed.",
|
||||
"jellyfin_entity": "The Jellyfin media_player for this Shield. Stream details come from the HA Jellyfin integration — no extra setup needed.",
|
||||
"receiver_entity": "The receiver or soundbar this Shield plays through.",
|
||||
"receiver_input": "The receiver input the Shield uses, for example 'Shield'.",
|
||||
"power_entity": "A smart plug switch, if you want reliable on/off detection.",
|
||||
"adb_enabled": "Reads now-playing information over ADB from any app. Requires Developer options > Network debugging on the Shield.",
|
||||
"adb_store_app_info": "Shows the title and artist from non-Jellyfin apps. Turn off to show only the app name (privacy mode).",
|
||||
"host": "The Shield's IP address. Change it only if the Shield moved or you gave it a different address.",
|
||||
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). Turning it on needs a one-time PIN pairing \u2014 use Reconfigure on the entry to pair.",
|
||||
"protocol_enabled": "Adds the all-in-one remote-protocol transport (power, availability, app push and volume readout). Turning it on needs a one-time PIN pairing — use Reconfigure on the entry to pair.",
|
||||
"adb_port": "The ADB port. The standard value is 5555.",
|
||||
"app_name_map": "One entry per line, package=name (for example ar.tvplayer.tv=TiviMate), with an optional |icon after the name. Leave blank to clear.",
|
||||
"adb_command_enabled": "Adds the nvidia_shield.adb_command service (a raw ADB shell command over the persistent connection). Security-relevant: leave it off unless you need it \u2014 the same opt-in posture as the core androidtv integration.",
|
||||
"channel_digit_delay": "Pause between the channel digits entered via play_media(channel) \u2014 0.05 to 1.0 seconds (default 0.1, the tested value).",
|
||||
"adb_command_enabled": "Adds the nvidia_shield.adb_command service (a raw ADB shell command over the persistent connection). Security-relevant: leave it off unless you need it — the same opt-in posture as the core androidtv integration.",
|
||||
"channel_digit_delay": "Pause between the channel digits entered via play_media(channel) — 0.05 to 1.0 seconds (default 0.1, the tested value).",
|
||||
"channel_send_enter": "Presses ENTER after the last digit (protocol key first, then the ADB keyevent fallback). Off by default, matching TiviMate's tested behavior.",
|
||||
"logcat_events": "Detects app changes instantly through a dedicated logcat stream (AdbDevice.streaming_shell). Off by default \u2014 polling keeps the state correct either way, and the integration falls back to polling if the stream keeps failing.",
|
||||
"logcat_events": "Detects app changes instantly through a dedicated logcat stream (AdbDevice.streaming_shell). Off by default — polling keeps the state correct either way, and the integration falls back to polling if the stream keeps failing.",
|
||||
"remote_command_script": "One script for remote.send_command. It replaces the key path when set and receives the call's own parameters as variables: command (the list of key commands), num_repeats, delay_secs and hold_secs."
|
||||
}
|
||||
},
|
||||
"connection": {
|
||||
"title": "Main setup",
|
||||
"description": "The Shield's address, the transports it is read over, and the devices this entry links to. Changes here are kept when you save.",
|
||||
"data": {
|
||||
"host": "The Shield's IP address",
|
||||
"adb_port": "ADB port",
|
||||
"adb_enabled": "Read state over ADB",
|
||||
"adb_store_app_info": "Show title and artist",
|
||||
"protocol_enabled": "Add the Nvidia Remote Protocol",
|
||||
"logcat_events": "Watch app launches over logcat",
|
||||
"adb_command_enabled": "Allow the adb_command action",
|
||||
"jellyfin_entity": "Jellyfin media player",
|
||||
"power_entity": "Power switch"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The Shield's IP address. Change it only if the Shield moved.",
|
||||
"adb_port": "The ADB port. The standard value is 5555 — change it only if ADB uses a custom port on your network.",
|
||||
"adb_enabled": "Reads now-playing information over ADB from any app. Requires Developer options > Network debugging on the Shield.",
|
||||
"adb_store_app_info": "Shows the title and artist from non-Jellyfin apps. Turn off to show only the app name (privacy mode).",
|
||||
"protocol_enabled": "The all-in-one remote-protocol transport (power, availability, app push and volume readout). It needs the one-time PIN pairing from setup.",
|
||||
"logcat_events": "Uses a second ADB stream to notice app launches the moment they happen. Leave off unless you want the faster app switch.",
|
||||
"adb_command_enabled": "Exposes the adb_command action for this entry's media player. Off by default; it runs arbitrary shell commands on the Shield.",
|
||||
"jellyfin_entity": "The Jellyfin media_player for this Shield. Leave blank to detect it automatically.",
|
||||
"power_entity": "A smart plug switch, if you want reliable on/off detection."
|
||||
}
|
||||
},
|
||||
"receiver": {
|
||||
"title": "Receiver",
|
||||
"description": "The receiver or soundbar this Shield plays through — a media_player, or a plain switch for a soundbar that only turns on and off.",
|
||||
"data": {
|
||||
"receiver_entity": "Receiver",
|
||||
"receiver_input": "Receiver input"
|
||||
},
|
||||
"data_description": {
|
||||
"receiver_entity": "The receiver, soundbar or speaker switch this Shield plays through. Leave blank if there is none.",
|
||||
"receiver_input": "The receiver input the Shield uses, for example 'Shield'. Leave blank if you are not sure."
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"title": "Apps",
|
||||
"description": "One app per line: the package id, an equals sign, the name to show, and optionally a pipe and an icon (a picture URL or a camera entity id). Blank uses the built-in list.",
|
||||
"data": {
|
||||
"app_name_map": "App names and icons"
|
||||
},
|
||||
"data_description": {
|
||||
"app_name_map": "One row per app, for example ar.tvplayer.tv=TiviMate|http://192.168.1.10/tivi.png. The name is what the source list, the Current App sensor and the media player show."
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"title": "Scripts",
|
||||
"description": "Optional: run a Home Assistant script instead of the built-in command, per command. Clear a row to go back to the built-in path.",
|
||||
"data": {
|
||||
"remote_command_script": "Script for remote.send_command",
|
||||
"script_turn_on": "turn_on script",
|
||||
"script_turn_off": "turn_off script",
|
||||
"script_volume_up": "volume_up script",
|
||||
"script_volume_down": "volume_down script",
|
||||
"script_volume_set": "volume_set script",
|
||||
"script_volume_mute": "volume_mute script",
|
||||
"script_select_source": "select_source script",
|
||||
"channel_digit_delay": "Delay between channel digits (seconds)",
|
||||
"channel_send_enter": "Send ENTER after channel digits"
|
||||
},
|
||||
"data_description": {
|
||||
"remote_command_script": "Replaces the whole remote.send_command path for this Shield. Leave blank to use the built-in key presses.",
|
||||
"channel_digit_delay": "The gap between the digits of a channel number, 0.05 to 1.0 seconds. Default 0.1.",
|
||||
"channel_send_enter": "Turn on if your channel app needs ENTER after the digits."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -155,19 +282,19 @@
|
||||
"exceptions": {
|
||||
"invalid_command": {
|
||||
"title": "Empty command",
|
||||
"message": "The command field is empty \u2014 nothing was sent."
|
||||
"message": "The command field is empty — nothing was sent."
|
||||
},
|
||||
"unsupported_target": {
|
||||
"title": "Unsupported entity for adb_command",
|
||||
"message": "{entity_id} is not an adb_command participant (its entry must enable the adb_command_enabled option) \u2014 command not sent."
|
||||
"message": "{entity_id} is not an adb_command participant (its entry must enable the adb_command_enabled option) — command not sent."
|
||||
},
|
||||
"adb_unavailable": {
|
||||
"title": "ADB unavailable",
|
||||
"message": "The ADB connection of {entity_id} is down \u2014 command not sent."
|
||||
"message": "The ADB connection of {entity_id} is down — command not sent."
|
||||
},
|
||||
"unknown_source": {
|
||||
"title": "Unknown source",
|
||||
"message": "'{source}' is not one of this Shield's apps. Use a name from the source list \u2014 the app names from the Apps map, or the built-in list when no map is set."
|
||||
"message": "'{source}' is not one of this Shield's apps. Use a name from the source list — the app names from the Apps map, or the built-in list when no map is set."
|
||||
},
|
||||
"command_script_failed": {
|
||||
"title": "Command script failed",
|
||||
|
||||
Reference in new issue
Block a user