fix AVR input swap
Deploy HEOS panel / deploy (push) Successful in 25s

This commit is contained in:
2026-09-15 17:20:24 +02:00
parent dc7f09052d
commit 6a0f1fa5e8
13 changed files with 272 additions and 410 deletions
+27 -59
View File
@@ -1,4 +1,4 @@
"""Stand-in Denon hardware: just enough HEOS and Telnet to test against.
"""Stand-in HEOS hardware: just enough of the CLI protocol to test against.
The grouping rules are the part worth pinning down -- what a set_group
call does to a stereo pair is the kind of thing you do not want to find
@@ -15,12 +15,22 @@ class FakeHeos(threading.Thread):
NAMES = {1: "Home Cinema", 2: "Lego Room", 3: "Denon Home 200 L", 4: "Denon Home 200 R"}
# What browse/browse?sid=<AVR pid> reports: HEOS's own list of the
# AVR's local inputs, already under whatever names you gave them in
# its setup menu -- HEOS carries the renamed labels itself.
AVR_INPUTS = [
{"name": "Z30 Pro", "mid": "inputs/mediaplayer"},
{"name": "Switch", "mid": "inputs/game"},
{"name": "LG G5", "mid": "inputs/tvaudio"},
]
def __init__(self):
super().__init__(daemon=True)
self.groups = {3: [3, 4]} # gid -> pids, leader first
self.volumes = {pid: 20 for pid in self.NAMES}
self.play_states = {pid: "play" for pid in self.NAMES}
self.group_volumes = {3: 25}
self.now_playing_mid = {1: "inputs/mediaplayer"} # pid -> what get_now_playing_media reports
self.commands = [] # everything we were asked to do
self.server = socket.socket()
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@@ -106,6 +116,22 @@ class FakeHeos(threading.Thread):
self.play_states[pid] = args["state"]
return self._ok(path, message=f"pid={pid}&state={args['state']}")
if path == "player/get_now_playing_media":
pid = int(args["pid"])
mid = self.now_playing_mid.get(pid, "")
name = next((s["name"] for s in self.AVR_INPUTS if s["mid"] == mid), mid)
return self._ok(path, payload={"mid": mid, "station": name} if mid else {})
if path == "browse/browse":
sid = int(args["sid"])
payload = list(self.AVR_INPUTS) if sid == 1 else []
return self._ok(path, payload=payload)
if path == "browse/play_input":
pid = int(args["pid"])
self.now_playing_mid[pid] = args["input"]
return self._ok(path)
if path.endswith("/toggle_mute") or path == "system/heart_beat":
return self._ok(path)
@@ -125,61 +151,3 @@ class FakeHeos(threading.Thread):
if payload is not None:
reply["payload"] = payload
return reply
class FakeAvr(threading.Thread):
"""A Denon Telnet server that knows SI and SSFUN."""
SOURCES = [("MPLAY", "Apple TV"), ("GAME", "PlayStation"),
("SAT/CBL", "TV Box"), ("DVD", "Old DVD")]
DELETED = {"DVD"} # switched off in the AVR's setup menu
def __init__(self):
super().__init__(daemon=True)
self.input = "MPLAY"
self.server = socket.socket()
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind(("127.0.0.1", 0))
self.server.listen(4)
self.port = self.server.getsockname()[1]
self.start()
def run(self):
while True:
try:
conn, _ = self.server.accept()
except OSError:
return
threading.Thread(target=self._serve, args=(conn,), daemon=True).start()
def _serve(self, conn):
buffer = b""
with conn:
while True:
try:
chunk = conn.recv(1024)
except OSError:
return
if not chunk:
return
buffer += chunk
while b"\r" in buffer:
line, buffer = buffer.split(b"\r", 1)
for reply in self.handle(line.decode().strip()):
conn.sendall(reply.encode() + b"\r")
def handle(self, command: str) -> list:
if command == "SSFUN ?":
# The real AVR pads the names out with spaces.
return [f"SSFUN{code} {name} " for code, name in self.SOURCES] + ["SSFUN END"]
if command == "SSSOD ?":
return [f"SSSOD{code} {'DEL' if code in self.DELETED else 'USE'}"
for code, _ in self.SOURCES] + ["SSSOD END"]
if command == "SI?":
return [f"SI{self.input}"]
if command.startswith("SI"):
self.input = command[2:]
return [f"SI{self.input}"]
if command == "PW?":
return ["PWON"]
return []