Fan out targeted services to multiple RelayTV entries

Resolve all targeted RelayTV config entries for service calls so overlay, play, snapshot, resume, and other targeted actions execute across multiple selected Home Assistant entities or devices instead of only the first target.
This commit is contained in:
2026-03-21 17:49:08 -05:00
parent 88743f8b0a
commit 792f6905ab
+86 -71
View File
@@ -125,36 +125,50 @@ def _fallback_entry_id(hass: HomeAssistant) -> str | None:
return None return None
def _resolve_entry_id_for_call(hass: HomeAssistant, call: ServiceCall) -> str | None: def _target_entity_ids_for_call(hass: HomeAssistant, call: ServiceCall) -> list[str]:
entity_ids: list[str] = [] entity_ids: list[str] = []
raw_entity = call.data.get(CONF_ENTITY_ID) raw_entity = call.data.get(CONF_ENTITY_ID)
if isinstance(raw_entity, str): if isinstance(raw_entity, str):
entity_ids = [raw_entity] entity_ids.append(raw_entity)
elif isinstance(raw_entity, list): elif isinstance(raw_entity, list):
entity_ids = [item for item in raw_entity if isinstance(item, str)] entity_ids.extend(item for item in raw_entity if isinstance(item, str))
registry = er.async_get(hass) registry = er.async_get(hass)
device_id = call.data.get("device_id")
device_ids = [device_id] if isinstance(device_id, str) else device_id
if isinstance(device_ids, list):
for item in device_ids:
if not isinstance(item, str):
continue
for reg_entry in er.async_entries_for_device(registry, item):
if reg_entry.entity_id.startswith("media_player."):
entity_ids.append(reg_entry.entity_id)
if not entity_ids: return list(dict.fromkeys(entity_ids))
device_id = call.data.get("device_id")
device_ids = [device_id] if isinstance(device_id, str) else device_id
if isinstance(device_ids, list):
for item in device_ids:
if not isinstance(item, str):
continue
for reg_entry in er.async_entries_for_device(registry, item):
if reg_entry.entity_id.startswith("media_player."):
entity_ids.append(reg_entry.entity_id)
for entity_id in entity_ids:
def _resolve_entry_ids_for_call(hass: HomeAssistant, call: ServiceCall) -> list[str]:
registry = er.async_get(hass)
entry_ids: list[str] = []
for entity_id in _target_entity_ids_for_call(hass, call):
reg_entry = registry.async_get(entity_id) reg_entry = registry.async_get(entity_id)
if reg_entry and _get_entry_data(hass, reg_entry.config_entry_id): if reg_entry and _get_entry_data(hass, reg_entry.config_entry_id):
return reg_entry.config_entry_id entry_ids.append(reg_entry.config_entry_id)
if entry_ids:
return list(dict.fromkeys(entry_ids))
panel_target = hass.data.get(DOMAIN, {}).get(DATA_PANEL_SETTINGS, {}).get(CONF_PANEL_TARGET_ENTRY_ID) panel_target = hass.data.get(DOMAIN, {}).get(DATA_PANEL_SETTINGS, {}).get(CONF_PANEL_TARGET_ENTRY_ID)
if panel_target and _get_entry_data(hass, panel_target): if panel_target and _get_entry_data(hass, panel_target):
return panel_target return [panel_target]
return _fallback_entry_id(hass)
fallback = _fallback_entry_id(hass)
return [fallback] if fallback else []
def _resolve_entry_id_for_call(hass: HomeAssistant, call: ServiceCall) -> str | None:
entry_ids = _resolve_entry_ids_for_call(hass, call)
return entry_ids[0] if entry_ids else None
def _resolve_entries_for_entities(hass: HomeAssistant, entity_ids: list[str]) -> list[str]: def _resolve_entries_for_entities(hass: HomeAssistant, entity_ids: list[str]) -> list[str]:
@@ -318,23 +332,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
url = (call.data.get("url") or "").strip() url = (call.data.get("url") or "").strip()
if not url: if not url:
return return
entry_id = _resolve_entry_id_for_call(hass, call) for entry_id in _resolve_entry_ids_for_call(hass, call):
store = _get_entry_data(hass, entry_id) if entry_id else None store = _get_entry_data(hass, entry_id)
if not store: if not store:
return continue
await store[DATA_API].smart_url(url) await store[DATA_API].smart_url(url)
await store[DATA_COORDINATOR].async_request_refresh() await store[DATA_COORDINATOR].async_request_refresh()
async def _handle_play_now(call: ServiceCall): async def _handle_play_now(call: ServiceCall):
url = (call.data.get("url") or "").strip() url = (call.data.get("url") or "").strip()
if not url: if not url:
return return
entry_id = _resolve_entry_id_for_call(hass, call) for entry_id in _resolve_entry_ids_for_call(hass, call):
store = _get_entry_data(hass, entry_id) if entry_id else None store = _get_entry_data(hass, entry_id)
if not store: if not store:
return continue
await store[DATA_API].play(url=url, use_ytdlp=call.data.get("use_ytdlp"), cec=call.data.get("cec")) await store[DATA_API].play(url=url, use_ytdlp=call.data.get("use_ytdlp"), cec=call.data.get("cec"))
await store[DATA_COORDINATOR].async_request_refresh() await store[DATA_COORDINATOR].async_request_refresh()
async def _handle_announce(call: ServiceCall): async def _handle_announce(call: ServiceCall):
await _handle_play_now(call) await _handle_play_now(call)
@@ -343,28 +357,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
url = (call.data.get("url") or "").strip() url = (call.data.get("url") or "").strip()
if not url: if not url:
return return
entry_id = _resolve_entry_id_for_call(hass, call) for entry_id in _resolve_entry_ids_for_call(hass, call):
store = _get_entry_data(hass, entry_id) if entry_id else None store = _get_entry_data(hass, entry_id)
if not store: if not store:
return continue
await store[DATA_API].play_temporary( await store[DATA_API].play_temporary(
url=url, url=url,
timeout_sec=call.data.get("timeout"), timeout_sec=call.data.get("timeout"),
volume_override=call.data.get("volume"), volume_override=call.data.get("volume"),
) )
await store[DATA_COORDINATOR].async_request_refresh() await store[DATA_COORDINATOR].async_request_refresh()
async def _handle_overlay(call: ServiceCall): async def _handle_overlay(call: ServiceCall):
entry_id = _resolve_entry_id_for_call(hass, call) for entry_id in _resolve_entry_ids_for_call(hass, call):
store = _get_entry_data(hass, entry_id) if entry_id else None store = _get_entry_data(hass, entry_id)
if not store: if not store:
return continue
await store[DATA_API].overlay( await store[DATA_API].overlay(
text=call.data.get("text"), text=call.data.get("text"),
duration=call.data.get("duration"), duration=call.data.get("duration"),
position=call.data.get("position"), position=call.data.get("position"),
image_url=call.data.get("image_url"), image_url=call.data.get("image_url"),
) )
async def _handle_play_synced(call: ServiceCall): async def _handle_play_synced(call: ServiceCall):
url = (call.data.get("url") or "").strip() url = (call.data.get("url") or "").strip()
@@ -372,13 +386,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return return
delay = float(call.data.get("delay_buffer_sec") or 2) delay = float(call.data.get("delay_buffer_sec") or 2)
start_at = time.time() + delay start_at = time.time() + delay
targets = call.data.get("target_entities") or call.data.get(CONF_ENTITY_ID) or [] targets = call.data.get("target_entities")
entry_ids: list[str] = []
if isinstance(targets, str): if isinstance(targets, str):
targets = [targets] targets = [targets]
entry_ids = _resolve_entries_for_entities(hass, [e for e in targets if isinstance(e, str)]) if isinstance(targets, list):
entry_ids = _resolve_entries_for_entities(hass, [e for e in targets if isinstance(e, str)])
if not entry_ids: if not entry_ids:
fallback = _resolve_entry_id_for_call(hass, call) entry_ids = _resolve_entry_ids_for_call(hass, call)
entry_ids = [fallback] if fallback else []
for entry_id in entry_ids: for entry_id in entry_ids:
store = _get_entry_data(hass, entry_id) store = _get_entry_data(hass, entry_id)
if not store: if not store:
@@ -387,31 +402,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await store[DATA_COORDINATOR].async_request_refresh() await store[DATA_COORDINATOR].async_request_refresh()
async def _handle_snapshot(call: ServiceCall): async def _handle_snapshot(call: ServiceCall):
entry_id = _resolve_entry_id_for_call(hass, call) for entry_id in _resolve_entry_ids_for_call(hass, call):
store = _get_entry_data(hass, entry_id) if entry_id else None store = _get_entry_data(hass, entry_id)
if not store: if not store:
return continue
data = await store[DATA_API].snapshot() or {} data = await store[DATA_API].snapshot() or {}
snapshot_url = data.get("image_url") if isinstance(data, dict) else None snapshot_url = data.get("image_url") if isinstance(data, dict) else None
snapshot_url = _absolute_url(store[DATA_API].base_url, snapshot_url) snapshot_url = _absolute_url(store[DATA_API].base_url, snapshot_url)
if snapshot_url: if snapshot_url:
store[DATA_LAST_SNAPSHOT_URL] = snapshot_url store[DATA_LAST_SNAPSHOT_URL] = snapshot_url
await store[DATA_COORDINATOR].async_request_refresh() await store[DATA_COORDINATOR].async_request_refresh()
async def _handle_play_with_resume(call: ServiceCall): async def _handle_play_with_resume(call: ServiceCall):
url = (call.data.get("url") or "").strip() url = (call.data.get("url") or "").strip()
if not url: if not url:
return return
entry_id = _resolve_entry_id_for_call(hass, call)
store = _get_entry_data(hass, entry_id) if entry_id else None
if not store:
return
runtime = await _async_load_runtime_data(hass) runtime = await _async_load_runtime_data(hass)
resume_position = runtime.get(CONF_RESUME_POSITIONS, {}).get(url) resume_position = runtime.get(CONF_RESUME_POSITIONS, {}).get(url)
await store[DATA_API].play(url=url, use_ytdlp=call.data.get("use_ytdlp"), cec=call.data.get("cec")) for entry_id in _resolve_entry_ids_for_call(hass, call):
if resume_position is not None: store = _get_entry_data(hass, entry_id)
await store[DATA_API].seek_abs(float(resume_position)) if not store:
await store[DATA_COORDINATOR].async_request_refresh() continue
await store[DATA_API].play(url=url, use_ytdlp=call.data.get("use_ytdlp"), cec=call.data.get("cec"))
if resume_position is not None:
await store[DATA_API].seek_abs(float(resume_position))
await store[DATA_COORDINATOR].async_request_refresh()
for service_name, handler in ( for service_name, handler in (
(SERVICE_SMART_URL, _handle_smart_url), (SERVICE_SMART_URL, _handle_smart_url),