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>
125 lines
4.0 KiB
Python
125 lines
4.0 KiB
Python
"""A pretend HEOS network, for `python3 app.py --demo`.
|
|
|
|
Lets you work on the interface on a laptop, with no speakers on the
|
|
network -- and lets the panel be tested without waking the house up.
|
|
"""
|
|
|
|
import time
|
|
|
|
from controller import stepped_level
|
|
|
|
|
|
class _FakeAvr:
|
|
INPUTS = [
|
|
{"code": "MPLAY", "name": "Apple TV"},
|
|
{"code": "GAME", "name": "PlayStation"},
|
|
{"code": "SAT/CBL", "name": "TV Box"},
|
|
{"code": "BD", "name": "Blu-ray"},
|
|
{"code": "TUNER", "name": "Radio"},
|
|
{"code": "PHONO", "name": "Turntable"},
|
|
]
|
|
|
|
def __init__(self, allowed_codes=()):
|
|
self.allowed_codes = list(allowed_codes or [])
|
|
self.connected = True
|
|
self._code = "MPLAY"
|
|
|
|
def inputs(self, refresh=False):
|
|
sources = list(self.INPUTS)
|
|
if self.allowed_codes:
|
|
order = {code: i for i, code in enumerate(self.allowed_codes)}
|
|
sources = sorted((s for s in sources if s["code"] in order), key=lambda s: order[s["code"]])
|
|
return sources
|
|
|
|
def name_for(self, code):
|
|
return next((s["name"] for s in self.INPUTS if s["code"] == code), code)
|
|
|
|
def current_input(self):
|
|
return {"code": self._code, "name": self.name_for(self._code)}
|
|
|
|
def select_input(self, code):
|
|
time.sleep(0.15) # the real AVR is not instant either
|
|
self._code = code
|
|
return self.current_input()
|
|
|
|
|
|
class DemoController:
|
|
def __init__(self, cfg):
|
|
self.cfg = cfg
|
|
self.avr = _FakeAvr(cfg.AVR_INPUT_CODES)
|
|
self.heos = None
|
|
self._volume = {key: 22 + 7 * i for i, key in enumerate(cfg.TARGETS)}
|
|
self._joined = set()
|
|
|
|
# -- what the UI uses ---------------------------------------------
|
|
def state(self):
|
|
return {
|
|
"host": {"key": self.cfg.HOST_KEY, "label": self.cfg.TARGETS[self.cfg.HOST_KEY]["label"]},
|
|
"rooms": [
|
|
{"key": key, "label": self.cfg.TARGETS[key]["label"], "available": True,
|
|
"grouped": key in self._joined, "volume": self._volume[key], "error": None}
|
|
for key in self.cfg.ROOM_KEYS
|
|
],
|
|
"avr": {"connected": True, "inputs": self.avr.inputs(), "input": self.avr.current_input()},
|
|
"heos_ok": True,
|
|
"errors": [],
|
|
"demo": True,
|
|
}
|
|
|
|
def volume(self, key):
|
|
return self._volume[key]
|
|
|
|
def set_volume(self, key, level):
|
|
self._volume[key] = max(0, min(100, int(level)))
|
|
return self._volume[key]
|
|
|
|
def nudge_volume(self, key, delta):
|
|
return self.set_volume(key, self._volume[key] + int(delta))
|
|
|
|
def step_volume(self, key, steps):
|
|
return self.set_volume(key, stepped_level(self._volume[key], int(steps), self.cfg.VOLUME_STEP))
|
|
|
|
def toggle_mute(self, key):
|
|
return None
|
|
|
|
def join(self, key):
|
|
self._joined.add(key)
|
|
return self.joined_keys()
|
|
|
|
def leave(self, key):
|
|
self._joined.discard(key)
|
|
return self.joined_keys()
|
|
|
|
def set_membership(self, joined):
|
|
self._joined = {k for k in self.cfg.ROOM_KEYS if k in joined}
|
|
return self.joined_keys()
|
|
|
|
def joined_keys(self):
|
|
return [k for k in self.cfg.ROOM_KEYS if k in self._joined]
|
|
|
|
# -- enough of the rest to keep the legacy routes answering --------
|
|
def scan(self):
|
|
return {
|
|
"players": [{"name": t["heos_name"], "pid": 1000 + i, "model": "Demo"}
|
|
for i, t in enumerate(self.cfg.TARGETS.values())],
|
|
"groups": [],
|
|
}
|
|
|
|
def group_targets(self, host_key, member_keys):
|
|
return self.set_membership(member_keys)
|
|
|
|
def ungroup(self, key):
|
|
return self.leave(key)
|
|
|
|
def play_state(self, key, state):
|
|
return None
|
|
|
|
def skip(self, key, direction):
|
|
return None
|
|
|
|
def heos_inputs(self, key):
|
|
return [{"name": s["name"], "input_id": f"inputs/{s['code'].lower()}"} for s in self.avr.inputs()]
|
|
|
|
def play_heos_input(self, key, input_id, source_key=None):
|
|
return None
|