Files
heos/demo.py
T
franzzandClaude Opus 5 1a1fcc84d6 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>
2026-09-14 21:39:31 +02:00

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