Skip to content

Commit 125d65a

Browse files
author
Ted Roberts
committed
Use screenLayerLayout for audio input switching
1 parent 624ab1b commit 125d65a

2 files changed

Lines changed: 75 additions & 189 deletions

File tree

custom_components/novastar_h/api.py

Lines changed: 74 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -576,12 +576,7 @@ def _coerce_audio_id(self, value: Any) -> int | None:
576576
return None
577577

578578
def _audio_inputs_from_layers(self, layers: list[dict[str, Any]]) -> list[dict[str, Any]]:
579-
"""Build audio input options from layers where audioStatus.isAvailable == 1.
580-
581-
Prefer layer source.inputId as the option id when available. Some firmware
582-
keeps audioStatus.isOpen anchored to one layer (commonly layer 0) while
583-
switching source input within that layer.
584-
"""
579+
"""Build audio input options from layers where audioStatus.isAvailable == 1."""
585580
mapped: dict[int, str] = {}
586581

587582
for layer in layers:
@@ -601,28 +596,21 @@ def _audio_inputs_from_layers(self, layers: list[dict[str, Any]]) -> list[dict[s
601596
continue
602597

603598
source = layer.get("source")
604-
option_id = layer_id
605599
input_name: str | None = None
606600
if isinstance(source, dict):
607-
source_input_id = self._coerce_audio_id(source.get("inputId"))
608-
if source_input_id is not None:
609-
option_id = source_input_id
610601
source_name = source.get("name")
611602
if isinstance(source_name, str) and source_name.strip():
612603
input_name = source_name.strip()
613604

614605
if not input_name:
615606
input_name = "Input"
616607

617-
mapped[option_id] = f"{input_name} (Layer {layer_id})"
608+
mapped[layer_id] = f"{input_name} (Layer {layer_id})"
618609

619-
return [{"id": option_id, "name": mapped[option_id]} for option_id in sorted(mapped)]
610+
return [{"id": layer_id, "name": mapped[layer_id]} for layer_id in sorted(mapped)]
620611

621612
def _selected_audio_input_from_layers(self, layers: list[dict[str, Any]]) -> int | None:
622-
"""Get selected audio input id from layer audioStatus.isOpen flag.
623-
624-
Prefer open layer source.inputId, fallback to open layerId.
625-
"""
613+
"""Get selected audio input id from layer audioStatus.isOpen flag."""
626614
selected_ids: list[int] = []
627615

628616
for layer in layers:
@@ -639,13 +627,7 @@ def _selected_audio_input_from_layers(self, layers: list[dict[str, Any]]) -> int
639627

640628
is_open = self._coerce_audio_id(audio_status.get("isOpen"))
641629
if is_open == 1:
642-
selected_id = layer_id
643-
source = layer.get("source")
644-
if isinstance(source, dict):
645-
source_input_id = self._coerce_audio_id(source.get("inputId"))
646-
if source_input_id is not None:
647-
selected_id = source_input_id
648-
selected_ids.append(selected_id)
630+
selected_ids.append(layer_id)
649631

650632
if not selected_ids:
651633
return None
@@ -768,197 +750,101 @@ async def async_set_audio_input(
768750
screen_id: int = 0,
769751
device_id: int = 0,
770752
) -> bool:
771-
"""Set active audio input.
772-
773-
The provided input_id may represent either a layerId or a source inputId.
774-
"""
775-
selected_option_id = int(input_id)
776-
layer_items = await self.async_get_layers_with_details(
777-
device_id=int(device_id),
778-
screen_id=int(screen_id),
779-
)
780-
781-
audio_layers: list[dict[str, Any]] = []
782-
for layer in layer_items:
783-
if not isinstance(layer, dict):
784-
continue
785-
layer_id = self._coerce_audio_id(layer.get("layerId"))
786-
if layer_id is None:
787-
continue
788-
audio_status = layer.get("audioStatus")
789-
if not isinstance(audio_status, dict):
790-
continue
791-
if self._coerce_audio_id(audio_status.get("isAvailable")) != 1:
792-
continue
793-
audio_layers.append(layer)
794-
795-
self._debug_log(
796-
"Audio input set request host=%s screen_id=%s device_id=%s selected_option_id=%s available_audio_layers=%s",
797-
self._host,
798-
int(screen_id),
799-
int(device_id),
800-
selected_option_id,
801-
[self._coerce_audio_id(layer.get("layerId")) for layer in audio_layers],
802-
)
803-
804-
if not audio_layers:
805-
self._debug_log("Audio input set aborted: no layers with audioStatus.isAvailable == 1")
806-
return False
807-
808-
def _layer_source_input_id(layer: dict[str, Any]) -> int | None:
809-
source = layer.get("source")
810-
if isinstance(source, dict):
811-
return self._coerce_audio_id(source.get("inputId"))
812-
return None
813-
814-
selected_layer: dict[str, Any] | None = None
815-
for layer in audio_layers:
816-
layer_id = self._coerce_audio_id(layer.get("layerId"))
817-
source_input_id = _layer_source_input_id(layer)
818-
if layer_id == selected_option_id or source_input_id == selected_option_id:
819-
selected_layer = layer
820-
break
753+
"""Set active audio input via layer/screenLayerLayout using layer detail list."""
754+
selected_layer_id = int(input_id)
755+
detail_payload = {
756+
"deviceId": int(device_id),
757+
"screenId": int(screen_id),
758+
}
821759

822-
if selected_layer is None:
760+
layout_data = await self._async_request("layer/detailList", detail_payload)
761+
if not isinstance(layout_data, dict):
823762
self._debug_log(
824-
"Audio input set aborted: selected_option_id=%s not matched to any available layer/source available=%s",
825-
selected_option_id,
826-
[
827-
{
828-
"layer_id": self._coerce_audio_id(layer.get("layerId")),
829-
"source_input_id": _layer_source_input_id(layer),
830-
}
831-
for layer in audio_layers
832-
],
763+
"Audio input set aborted: layer/detailList returned invalid data host=%s",
764+
self._host,
833765
)
834766
return False
835767

836-
selected_layer_id = self._coerce_audio_id(selected_layer.get("layerId"))
837-
if selected_layer_id is None:
768+
layers_key = "screenLayers" if isinstance(layout_data.get("screenLayers"), list) else "layers"
769+
raw_layers = layout_data.get(layers_key)
770+
if not isinstance(raw_layers, list):
838771
self._debug_log(
839-
"Audio input set aborted: matched layer has no valid layerId selected_option_id=%s",
840-
selected_option_id,
772+
"Audio input set aborted: no layers in detailList host=%s keys=%s",
773+
self._host,
774+
list(layout_data.keys()),
841775
)
842776
return False
843777

844-
any_layer_updated = False
845-
selected_layer_updated = False
846-
847-
async def _build_write_general_payload(
848-
layer: dict[str, Any], should_open: int
849-
) -> dict[str, Any] | None:
850-
"""Build layer/writeGeneral payload for one layer audio open state."""
851-
layer_id = self._coerce_audio_id(layer.get("layerId"))
852-
current_audio_status = layer.get("audioStatus")
853-
if layer_id is None or not isinstance(current_audio_status, dict):
854-
return None
855-
856-
desired_audio_status = dict(current_audio_status)
857-
desired_audio_status["isOpen"] = should_open
858-
859-
payload_base = {
860-
"screenId": int(screen_id),
861-
"deviceId": int(device_id),
862-
"layerId": int(layer_id),
863-
}
778+
updated_layers: list[dict[str, Any]] = []
779+
selected_found = False
780+
for layer in raw_layers:
781+
if not isinstance(layer, dict):
782+
continue
783+
layer_copy = dict(layer)
784+
layer_id = self._coerce_audio_id(layer_copy.get("layerId"))
785+
audio_status = layer_copy.get("audioStatus")
786+
if not isinstance(audio_status, dict):
787+
audio_status = {}
788+
updated_audio_status = dict(audio_status)
789+
if layer_id == selected_layer_id:
790+
updated_audio_status["isOpen"] = 1
791+
selected_found = True
792+
else:
793+
updated_audio_status["isOpen"] = 0
794+
layer_copy["audioStatus"] = updated_audio_status
795+
updated_layers.append(layer_copy)
864796

865-
general_data: dict[str, Any] | None = None
866-
layer_detail = await self.async_get_layer_detail(
867-
layer_id=int(layer_id),
868-
device_id=int(device_id),
869-
screen_id=int(screen_id),
797+
if not selected_found:
798+
self._debug_log(
799+
"Audio input set aborted: selected layer not found selected_layer_id=%s available=%s",
800+
selected_layer_id,
801+
[self._coerce_audio_id(layer.get("layerId")) for layer in updated_layers],
870802
)
871-
if isinstance(layer_detail, dict) and isinstance(layer_detail.get("general"), dict):
872-
general_data = dict(layer_detail["general"])
873-
elif isinstance(layer.get("general"), dict):
874-
general_data = dict(layer["general"])
875-
876-
if general_data is None:
877-
self._debug_log(
878-
"Audio input write skipped layer_id=%s: missing general data for layer/writeGeneral",
879-
layer_id,
880-
)
881-
return None
882-
883-
write_general_payload = {
884-
**payload_base,
885-
"name": general_data.get("name")
886-
if isinstance(general_data.get("name"), str) and general_data.get("name").strip()
887-
else f"Layer {layer_id}",
888-
"sizeType": int(general_data.get("sizeType", 0)),
889-
"type": int(general_data.get("type", 0)),
890-
"zorder": int(general_data.get("zorder", 0)),
891-
"isBackground": bool(general_data.get("isBackground", False)),
892-
"isFreeze": bool(general_data.get("isFreeze", False)),
893-
"flipType": int(general_data.get("flipType", 0)),
894-
"audioStatus": desired_audio_status,
895-
}
896-
897-
optional_lock = self._coerce_audio_id(general_data.get("lock"))
898-
if optional_lock is not None:
899-
write_general_payload["lock"] = optional_lock
900-
901-
reverse_control = layer_detail.get("reverseControl") if isinstance(layer_detail, dict) else None
902-
if isinstance(reverse_control, dict):
903-
write_general_payload["reverseControl"] = reverse_control
904-
return write_general_payload
905-
906-
for layer in audio_layers:
907-
layer_id = self._coerce_audio_id(layer.get("layerId"))
908-
current_audio_status = layer.get("audioStatus")
909-
if layer_id is None or not isinstance(current_audio_status, dict):
910-
continue
911-
should_open = 1 if layer is selected_layer else 0
912-
payload = await _build_write_general_payload(layer, should_open)
913-
if payload is None:
914-
continue
915-
attempt = await self._async_request("layer/writeGeneral", payload)
916-
if attempt is not None:
917-
any_layer_updated = True
918-
if should_open == 1:
919-
selected_layer_updated = True
803+
return False
920804

921-
if any_layer_updated:
922-
self._force_refresh_layer_details = True
805+
write_payload = {
806+
**detail_payload,
807+
**{
808+
key: value
809+
for key, value in layout_data.items()
810+
if key not in ("screenLayers", "layers")
811+
},
812+
layers_key: updated_layers,
813+
}
923814

924-
async def _verify_selected_open() -> tuple[bool, int | None, list[int | None]]:
925-
self._force_refresh_layer_details = True
926-
refreshed_layers = await self.async_get_layers_with_details(
927-
device_id=int(device_id),
928-
screen_id=int(screen_id),
815+
write_result = await self._async_request("layer/screenLayerLayout", write_payload)
816+
if write_result is None:
817+
_LOGGER.warning(
818+
"Audio input apply failed on host=%s selected_layer_id=%s endpoint=layer/screenLayerLayout",
819+
self._host,
820+
selected_layer_id,
929821
)
930-
selected_after = self._selected_audio_input_from_layers(refreshed_layers)
822+
return False
823+
824+
self._force_refresh_layer_details = True
825+
refreshed_layers = await self.async_get_layers_with_details(
826+
device_id=int(device_id),
827+
screen_id=int(screen_id),
828+
)
829+
selected_after = self._selected_audio_input_from_layers(refreshed_layers)
830+
if selected_after != selected_layer_id:
931831
open_layers = [
932832
self._coerce_audio_id(layer.get("layerId"))
933833
for layer in refreshed_layers
934834
if isinstance(layer, dict)
935835
and isinstance(layer.get("audioStatus"), dict)
936836
and self._coerce_audio_id(layer["audioStatus"].get("isOpen")) == 1
937837
]
938-
is_selected = selected_after in (selected_option_id, selected_layer_id)
939-
return is_selected, selected_after, open_layers
940-
941-
is_selected_applied, selected_after_write, currently_open = await _verify_selected_open()
942-
943-
if not is_selected_applied:
944838
_LOGGER.warning(
945-
"Audio input apply failed on host=%s selected_option_id=%s selected_layer_id=%s selected_after_write=%s open_layers=%s",
839+
"Audio input apply failed on host=%s selected_layer_id=%s selected_after_write=%s open_layers=%s endpoint=layer/screenLayerLayout",
946840
self._host,
947-
selected_option_id,
948841
selected_layer_id,
949-
selected_after_write,
950-
currently_open,
842+
selected_after,
843+
open_layers,
951844
)
845+
return False
952846

953-
self._debug_log(
954-
"Audio input write complete selected_layer_id=%s any_layer_updated=%s selected_layer_updated=%s selected_after_write=%s",
955-
selected_layer_id,
956-
any_layer_updated,
957-
selected_layer_updated,
958-
selected_after_write,
959-
)
960-
961-
return any_layer_updated and selected_layer_updated and is_selected_applied
847+
return True
962848

963849
async def async_set_audio_output(
964850
self,

custom_components/novastar_h/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@
2121
}
2222
],
2323
"zeroconf": ["_novastar._tcp.local."],
24-
"version": "0.2.53"
24+
"version": "0.2.54"
2525
}

0 commit comments

Comments
 (0)