+81
-8
@@ -1,4 +1,4 @@
|
||||
"""The actual behaviour of the panel, on top of the two protocol clients.
|
||||
"""The actual behaviour of the panel, on top of the HEOS CLI client.
|
||||
|
||||
The interesting part is grouping. A HEOS "In-Room Group" (your pair of
|
||||
Home 200s) is addressed by a gid and behaves like one speaker -- until
|
||||
@@ -10,6 +10,12 @@ Two consequences drive most of the code below:
|
||||
or the second Home 200 gets left behind.
|
||||
* Unmerging must re-issue set_group with the pair's own pids to put
|
||||
the pair back together, so we have to remember what they were.
|
||||
|
||||
Everything, including the AVR's own inputs, goes over the one HEOS
|
||||
connection now -- there used to be a second client here for the AVR's
|
||||
Denon Telnet port, only for its renamed input list, but HEOS reports
|
||||
those same renamed names itself (browse/browse on the AVR's own pid),
|
||||
so the Telnet side added a protocol for no remaining benefit.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -17,7 +23,6 @@ import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from avr import AvrControl, AvrError
|
||||
from heos import HeosClient, HeosError, parse_message
|
||||
|
||||
MEMBERS_FILE = Path(__file__).with_name("members.json")
|
||||
@@ -50,7 +55,6 @@ class Controller:
|
||||
def __init__(self, cfg):
|
||||
self.cfg = cfg
|
||||
self.heos = HeosClient(cfg.HEOS_HOST, cfg.HEOS_PORT)
|
||||
self.avr = AvrControl(cfg.AVR_HOST, cfg.AVR_PORT, cfg.AVR_INPUT_CODES)
|
||||
self._lock = threading.RLock()
|
||||
self._members_file = Path(getattr(cfg, "MEMBERS_FILE", MEMBERS_FILE))
|
||||
self._learned = _load_learned(self._members_file)
|
||||
@@ -312,8 +316,26 @@ class Controller:
|
||||
wanted = set(self.joined_keys()) | {key}
|
||||
self.group_targets(self.cfg.HOST_KEY, [k for k in self.cfg.ROOM_KEYS if k in wanted])
|
||||
self.scan()
|
||||
self._nudge_avr_input()
|
||||
return self.joined_keys()
|
||||
|
||||
def _nudge_avr_input(self):
|
||||
"""A room that has just joined the AVR's group sometimes stays
|
||||
silent on it until the AVR's input is reselected -- but that has to
|
||||
happen the way the HEOS app does it (browse/play_input, over HEOS
|
||||
itself) to actually push audio to the new member. Re-issuing the
|
||||
input over the AVR's own Telnet port does not: that just tells the
|
||||
AVR which of its jacks to listen to, it says nothing to HEOS about
|
||||
who should be streaming it. Best-effort: a join has already
|
||||
succeeded by the time this runs, so a HEOS hiccup here should not
|
||||
turn it into a failure."""
|
||||
try:
|
||||
current = self.avr_current_input()
|
||||
if current:
|
||||
self.play_heos_input(self.cfg.HOST_KEY, current["code"])
|
||||
except HeosError:
|
||||
pass
|
||||
|
||||
def leave(self, key: str) -> list:
|
||||
"""Remove one room. Deliberately does not rewrite the AVR's group:
|
||||
whatever is still joined keeps playing without a hiccup."""
|
||||
@@ -378,7 +400,10 @@ class Controller:
|
||||
self.heos.command(f"player/{command}", pid=self.playback_pid(key))
|
||||
|
||||
def heos_inputs(self, key: str) -> list:
|
||||
"""Physical inputs as HEOS sees them (generic ids, not your names)."""
|
||||
"""A player's physical inputs, as HEOS itself lists them -- under
|
||||
whatever names you gave them in the AVR's own setup menu. HEOS
|
||||
carries those renamed labels, not just its generic ids, so this is
|
||||
the same list the HEOS app itself shows under Sources."""
|
||||
with self._lock:
|
||||
self._fresh()
|
||||
reply = self.heos.command("browse/browse", sid=self.playback_pid(key))
|
||||
@@ -392,6 +417,54 @@ class Controller:
|
||||
params["spid"] = self.playback_pid(source_key)
|
||||
self.heos.command("browse/play_input", **params)
|
||||
|
||||
# -- the AVR's inputs, all of it over HEOS ------------------------------
|
||||
def avr_connected(self) -> bool:
|
||||
with self._lock:
|
||||
self._fresh()
|
||||
try:
|
||||
self._host_pid()
|
||||
return True
|
||||
except TargetError:
|
||||
return False
|
||||
|
||||
def avr_inputs(self) -> list:
|
||||
"""{"code", "name"} pairs for the picker -- heos_inputs()'s shape,
|
||||
renamed to match what the UI and /api/avr/* already send and
|
||||
expect. Narrowed and ordered by AVR_INPUT_CODES, same as before,
|
||||
except the codes it matches are now HEOS's own ("inputs/aux_in_1"),
|
||||
not the AVR's Telnet ones ("AUX1")."""
|
||||
sources = self.heos_inputs(self.cfg.HOST_KEY)
|
||||
codes = getattr(self.cfg, "AVR_INPUT_CODES", None)
|
||||
if codes:
|
||||
order = {code: i for i, code in enumerate(codes)}
|
||||
sources = sorted(
|
||||
(s for s in sources if s["input_id"] in order),
|
||||
key=lambda s: order[s["input_id"]],
|
||||
)
|
||||
return [{"code": s["input_id"], "name": s["name"]} for s in sources]
|
||||
|
||||
def avr_current_input(self):
|
||||
"""{"code", "name"} for whatever the AVR is playing right now, or
|
||||
None if that is not a local input (or the AVR is unreachable)."""
|
||||
with self._lock:
|
||||
self._fresh()
|
||||
try:
|
||||
reply = self.heos.command(
|
||||
"player/get_now_playing_media", pid=self._host_pid()
|
||||
)
|
||||
except (TargetError, HeosError):
|
||||
return None
|
||||
payload = reply.get("payload") or {}
|
||||
mid = payload.get("mid", "")
|
||||
if not mid.startswith("inputs/"):
|
||||
return None
|
||||
return {"code": mid, "name": payload.get("station") or payload.get("song") or mid}
|
||||
|
||||
def avr_select_input(self, code: str) -> dict:
|
||||
self.play_heos_input(self.cfg.HOST_KEY, code)
|
||||
name = next((s["name"] for s in self.avr_inputs() if s["code"] == code), code)
|
||||
return {"code": code, "name": name}
|
||||
|
||||
# -- one snapshot for the UI ------------------------------------------
|
||||
def state(self) -> dict:
|
||||
snapshot = {
|
||||
@@ -435,11 +508,11 @@ class Controller:
|
||||
|
||||
try:
|
||||
snapshot["avr"] = {
|
||||
"connected": self.avr.connected,
|
||||
"inputs": self.avr.inputs(),
|
||||
"input": self.avr.current_input(),
|
||||
"connected": self.avr_connected(),
|
||||
"inputs": self.avr_inputs(),
|
||||
"input": self.avr_current_input(),
|
||||
}
|
||||
except (AvrError, OSError) as exc:
|
||||
except (HeosError, TargetError) as exc:
|
||||
snapshot["errors"].append(str(exc))
|
||||
|
||||
return snapshot
|
||||
|
||||
Reference in New Issue
Block a user