Optimize api/state
Deploy HEOS panel / deploy (push) Successful in 26s

This commit is contained in:
2026-09-17 16:15:59 +02:00
parent 74c232e678
commit 053be20b9c
2 changed files with 157 additions and 12 deletions
+66
View File
@@ -127,6 +127,72 @@ class HeosClient:
raise HeosError(_failure_text(heos))
return reply
def command_batch(self, requests: list) -> list:
"""Send several commands back-to-back on the one socket, then
collect their replies as they come in -- so N independent reads
(a room's volume, play state and now-playing, say) cost one round
trip's worth of latency instead of N.
`requests` is a list of (path, params) pairs. Returns one entry
per request, in the same order: the parsed reply dict, or the
HeosError it failed with -- a single command failing does not
sink the rest of the batch. Only a connection-level problem
raises, same as command().
"""
with self._lock:
for attempt in (1, 2):
try:
if self._sock is None:
self._connect()
return self._exchange_batch(requests)
except (OSError, ValueError) as exc:
self._close()
if attempt == 2:
raise HeosError(f"cannot reach HEOS at {self.host}: {exc}") from exc
def _exchange_batch(self, requests: list) -> list:
for path, params in requests:
query = "&".join(f"{k}={v}" for k, v in params.items() if v is not None)
command = f"heos://{path}" + (f"?{query}" if query else "")
self._sock.sendall(command.encode("utf-8") + b"\r\n")
results = [None] * len(requests)
pending = list(range(len(requests)))
deadline = time.monotonic() + self.timeout * 3
while pending:
reply = json.loads(self._read_line(deadline).decode("utf-8"))
heos = reply.get("heos", {})
if "under process" in heos.get("message", ""):
continue # placeholder ack; the real payload follows
index = self._claim(pending, requests, heos)
if index is None:
self._watch_progress(heos)
continue # an event, or a reply to a command outside this batch
pending.remove(index)
results[index] = HeosError(_failure_text(heos)) if heos.get("result") == "fail" else reply
return results
@staticmethod
def _claim(pending, requests, heos):
"""Which pending request this reply answers. Matches on path
first; when more than one pending request shares a path (e.g.
get_now_playing_media for two different pids), whichever id
parameter -- pid/gid/sid -- the request carried ties it to the
right reply, since HEOS echoes it back in the message."""
candidates = [i for i in pending if requests[i][0] == heos.get("command")]
if not candidates:
return None
if len(candidates) == 1:
return candidates[0]
message = parse_message(heos.get("message", ""))
for i in candidates:
params = requests[i][1]
for id_key in ("pid", "gid", "sid"):
if id_key in params and message.get(id_key) == str(params[id_key]):
return i
return candidates[0] # can't tell them apart; oldest first
def _watch_progress(self, heos: dict):
"""Skims a passing player_now_playing_progress event for its
pid/cur_pos/duration, the only source of song-position data this