Snap volume to whole steps, and hide deleted AVR sources
A tap now moves to the next multiple of VOLUME_STEP rather than adding it, so 23 goes to 25 and 25 goes to 30 and the levels stay round. The panel counts taps and lets the speakers do the rounding from whatever level they are actually at, since the phone's copy can be seconds old; the same rule is mirrored in JS so the optimistic number never has to correct itself when the reply lands. The input picker also asks the AVR which sources are still switched on (SSSOD ?) and leaves out the ones deleted in its setup menu. Sources it does not mention are kept, so a model that ignores the command shows its whole list rather than nothing; deleted sources also keep their names, in case the AVR is sitting on one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -163,6 +163,24 @@ def parse_ssfun(lines: list) -> list:
|
||||
return sources
|
||||
|
||||
|
||||
def parse_sssod(lines: list) -> dict:
|
||||
"""Parse `SSSOD ?` output -- 'SSSODTUNER DEL' and friends -- into
|
||||
{"TUNER": False, "CD": True, ...}, i.e. which sources you have left
|
||||
switched on in the AVR's own setup menu."""
|
||||
usage = {}
|
||||
for line in lines:
|
||||
if not line.startswith("SSSOD"):
|
||||
continue
|
||||
rest = line[len("SSSOD"):].strip()
|
||||
if rest in ("END", ""):
|
||||
continue
|
||||
code, _, value = rest.rpartition(" ")
|
||||
code = code.strip()
|
||||
if code:
|
||||
usage[code] = value.strip().upper() != "DEL"
|
||||
return usage
|
||||
|
||||
|
||||
class AvrControl:
|
||||
"""The input list and the current input, in the names you chose."""
|
||||
|
||||
@@ -170,14 +188,15 @@ class AvrControl:
|
||||
self.telnet = DenonTelnet(host, port)
|
||||
self.allowed_codes = list(allowed_codes or [])
|
||||
self._inputs = None
|
||||
self._usage = None
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
return self.telnet.connected
|
||||
|
||||
def inputs(self, refresh: bool = False) -> list:
|
||||
"""Your renamed source list. Cached: it only changes when you
|
||||
rename something in the AVR's own setup menu."""
|
||||
def all_inputs(self, refresh: bool = False) -> list:
|
||||
"""Every source the AVR knows, under your names, deleted ones
|
||||
included. Cached: it only changes when you edit the setup menu."""
|
||||
if self._inputs is None or refresh:
|
||||
lines = self.telnet.request(
|
||||
"SSFUN ?", prefix="SSFUN",
|
||||
@@ -187,7 +206,24 @@ class AvrControl:
|
||||
sources = parse_ssfun(lines)
|
||||
if sources:
|
||||
self._inputs = sources
|
||||
sources = self._inputs or []
|
||||
if self._usage is None or refresh:
|
||||
lines = self.telnet.request(
|
||||
"SSSOD ?", prefix="SSSOD",
|
||||
until=lambda line: line.strip() == "SSSOD END",
|
||||
timeout=3.0,
|
||||
)
|
||||
self._usage = parse_sssod(lines)
|
||||
return self._inputs or []
|
||||
|
||||
def inputs(self, refresh: bool = False) -> list:
|
||||
"""What the picker offers: the sources you can actually select.
|
||||
|
||||
Sources you deleted in the AVR's setup menu are left out -- they
|
||||
are exactly the ones you never want to land on. Anything SSSOD
|
||||
does not mention is kept, so a model that does not answer that
|
||||
command shows its whole list rather than nothing at all.
|
||||
"""
|
||||
sources = [s for s in self.all_inputs(refresh) if self._usage.get(s["code"], True)]
|
||||
if self.allowed_codes:
|
||||
order = {code: i for i, code in enumerate(self.allowed_codes)}
|
||||
sources = sorted(
|
||||
@@ -208,7 +244,7 @@ class AvrControl:
|
||||
return {"code": code, "name": self.name_for(code)}
|
||||
|
||||
def name_for(self, code: str) -> str:
|
||||
for source in self.inputs():
|
||||
for source in self.all_inputs():
|
||||
if source["code"] == code:
|
||||
return source["name"]
|
||||
return code
|
||||
|
||||
Reference in New Issue
Block a user