@@ -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
|
||||
|
||||
Reference in New Issue
Block a user