feat: standards polish — G7 sensor translation_key naming + entity strings, G10 protocol-push device identity on the registry card, G12 unrecorded poll-driven attributes (media player DAG internals + sensors), G14 async_redact_data with an explicit redaction list behind the marker scan (t30)
This commit is contained in:
1 parent
194a97ce34
commit
2e70587871
8 files changed
+178
-5
No files matched your search
@@ -215,6 +215,9 @@ class ShieldConnectionManager:
|
||||
self._logcat_disabled = False
|
||||
self._logcat_failures: list[float] = []
|
||||
self._logcat_last_pkg: str | None = None
|
||||
# G10 (t30): protocol device identity captured once per session —
|
||||
# (name, manufacturer, model, sw) for the registry's device card.
|
||||
self.protocol_device_data: tuple[str, str, str, str] | None = None
|
||||
self._logcat_last_stamp: float = -1e9
|
||||
# Injectable ladder sleep (t26: deterministic tests / future alt-floors)
|
||||
self._logcat_ladder_sleep = asyncio.sleep
|
||||
@@ -565,6 +568,24 @@ class ShieldConnectionManager:
|
||||
self._app_label_requests.add(package)
|
||||
return None
|
||||
|
||||
async def async_capture_protocol_device_data(self) -> None:
|
||||
"""Capture (name, manufacturer, model, sw) once (t30 G10)."""
|
||||
if self._protocol is None or self.protocol_device_data is not None:
|
||||
return
|
||||
try:
|
||||
info = self._protocol.device_info or {} # {manufacturer, model, sw_version}
|
||||
name, _mac = await asyncio.wait_for(
|
||||
self._protocol.async_get_name_and_mac(), timeout=10.0
|
||||
)
|
||||
self.protocol_device_data = (
|
||||
name,
|
||||
info.get("manufacturer", "Google"),
|
||||
info.get("model", "Shield TV"),
|
||||
info.get("sw_version", ""),
|
||||
)
|
||||
except Exception as err:
|
||||
_LOGGER.debug("protocol device info unavailable: %s", err)
|
||||
|
||||
async def _fetch_app_label(self, package: str) -> None:
|
||||
"""One `pm dump` label fetch; failure falls through silently."""
|
||||
poller = self._poller
|
||||
@@ -997,6 +1018,14 @@ class ShieldConnectionManager:
|
||||
foreground = await poller.poll_foreground()
|
||||
if foreground:
|
||||
self.store.set(FIELD_FOREGROUND, foreground)
|
||||
# G10: capture the protocol device identity for the registry card
|
||||
if self._protocol is not None:
|
||||
self._tasks.append(
|
||||
self._create_task(
|
||||
self.async_capture_protocol_device_data(),
|
||||
"protocol_device_capture",
|
||||
)
|
||||
)
|
||||
# P5-1: manifest labels resolve on demand, one package per cycle
|
||||
self._drain_app_label_requests()
|
||||
# P5-3: ensure the (optional, default-off) logcat fast-path runs
|
||||
|
||||
@@ -22,8 +22,19 @@ from .connection import ShieldConnectionManager
|
||||
_REDACT_KEY_MARKERS = ("pairing", "pin", "secret", "password", "token", "key_material")
|
||||
_REDACTED = "**REDACTED**"
|
||||
|
||||
# G14: the documented explicit redaction field list (host paths stay
|
||||
# readable; pairing/key-shaped fields are masked by async_redact_data).
|
||||
_TO_REDACT = {
|
||||
"pairing_code",
|
||||
"adb_pairing_code",
|
||||
"protocol_pairing_code",
|
||||
"serial_number",
|
||||
"uuid",
|
||||
}
|
||||
|
||||
|
||||
def _redact(obj):
|
||||
"""Marker-based defensive pass (kept behind the async_redact_data helper)."""
|
||||
"""Redact sensitive keys from a JSON-ish diagnostics payload."""
|
||||
if isinstance(obj, dict):
|
||||
return {
|
||||
@@ -72,6 +83,14 @@ async def async_get_config_entry_diagnostics(
|
||||
"is_on": manager.is_on,
|
||||
},
|
||||
}
|
||||
# G14 (t30): the documented helper is the primary redaction path with
|
||||
# an explicit field list; the marker scan above runs defensively after.
|
||||
try:
|
||||
from homeassistant.util.diagnostic import async_redact_data
|
||||
except ImportError:
|
||||
async_redact_data = None
|
||||
if async_redact_data is not None:
|
||||
payload = async_redact_data(payload, _TO_REDACT)
|
||||
return _redact(payload)
|
||||
|
||||
|
||||
|
||||
@@ -216,6 +216,22 @@ class NvidiaShieldMediaPlayer(MediaPlayerEntity):
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
# G10 (t30): when the protocol transport is live and captured, the
|
||||
# registry card carries the protocol-pushed identity (name from the
|
||||
# device itself, manufacturer 'Google' per the audit ruling); the
|
||||
# pre-capture shape stays the entry-title fallback.
|
||||
captured = (
|
||||
self._manager.protocol_device_data if self._manager is not None else None
|
||||
)
|
||||
if captured:
|
||||
name, manufacturer, model, sw = captured
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._entry.entry_id)},
|
||||
name=name or self._entry.title,
|
||||
manufacturer=manufacturer,
|
||||
model=model,
|
||||
sw_version=sw or None,
|
||||
)
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._entry.entry_id)},
|
||||
name=self._entry.title,
|
||||
@@ -794,7 +810,18 @@ class NvidiaShieldMediaPlayer(MediaPlayerEntity):
|
||||
last_known=self._manager.last_known_map(),
|
||||
)
|
||||
|
||||
@property
|
||||
# G12 (t30): attributes that change with every poll must not bloat the
|
||||
# database (entity doc) — dag internals are declared unrecorded.
|
||||
_unrecorded_attributes = frozenset(
|
||||
{
|
||||
ATTR_MEDIA_DAG_NOW_PLAYING,
|
||||
ATTR_MEDIA_DAG_RUNNING_APPS,
|
||||
ATTR_MEDIA_DAG_RECEIVER_ENTITY,
|
||||
ATTR_MEDIA_DAG_RECEIVER_INPUT,
|
||||
ATTR_MEDIA_DAG_ROLE,
|
||||
}
|
||||
)
|
||||
|
||||
def extra_state_attributes(self) -> dict:
|
||||
# Now playing: prefer Jellyfin, fall back to ADB media session,
|
||||
# then the foreground tier (P3-3a).
|
||||
|
||||
@@ -39,6 +39,8 @@ class NvidiaShieldSensorBase(SensorEntity):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
# G12 (t30): poll-driven attributes never belong in the database
|
||||
_unrecorded_attributes = frozenset({"app_id", "app_name", "playback_state"})
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
self.hass = hass
|
||||
@@ -68,7 +70,7 @@ class NvidiaShieldCurrentAppSensor(NvidiaShieldSensorBase):
|
||||
(most recent visible ADB recent-task app).
|
||||
"""
|
||||
|
||||
_attr_name = "Current App"
|
||||
_attr_translation_key = "current_app"
|
||||
_attr_icon = "mdi:application"
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
@@ -136,7 +138,7 @@ class NvidiaShieldCurrentAppSensor(NvidiaShieldSensorBase):
|
||||
class NvidiaShieldNowPlayingSensor(NvidiaShieldSensorBase):
|
||||
"""What's currently playing via Jellyfin."""
|
||||
|
||||
_attr_name = "Now Playing"
|
||||
_attr_translation_key = "now_playing"
|
||||
_attr_icon = "mdi:play-circle"
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
|
||||
@@ -132,5 +132,15 @@
|
||||
"title": "ADB unavailable",
|
||||
"message": "The ADB connection of {entity_id} is down \u2014 command not sent."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"current_app": {
|
||||
"name": "Current app"
|
||||
},
|
||||
"now_playing": {
|
||||
"name": "Now playing"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,5 +132,15 @@
|
||||
"title": "ADB unavailable",
|
||||
"message": "The ADB connection of {entity_id} is down \u2014 command not sent."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"current_app": {
|
||||
"name": "Current app"
|
||||
},
|
||||
"now_playing": {
|
||||
"name": "Now playing"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user