fix AVR input swap
Deploy HEOS panel / deploy (push) Successful in 25s

This commit is contained in:
2026-09-15 17:20:24 +02:00
parent dc7f09052d
commit 6a0f1fa5e8
13 changed files with 272 additions and 410 deletions
+37 -20
View File
@@ -2,7 +2,11 @@
"""HEOS panel: a phone-sized web remote plus the HTTP bridge it runs on.
pip3 install -r requirements.txt
python3 app.py # http://<pi-ip>:5443/
python3 app.py # dev server, http://<pi-ip>:5443/
The service instead runs this under gunicorn -- see deploy/heos-panel.service
-- which imports `app` without ever calling main(), so the controller below
is built at import time rather than from main()'s argparse.
Everything the UI does goes through /api/*. The flatter, query-string
endpoints from the original heos_bridge.py (/volume/up?target=..., and
@@ -10,13 +14,13 @@ friends) are still here so existing Shortcuts and scripts keep working.
"""
import argparse
import os
from functools import wraps
from flask import Flask, jsonify, render_template, request
from werkzeug.middleware.proxy_fix import ProxyFix
import config
from avr import AvrError
from controller import Controller, TargetError
from heos import HeosError
@@ -30,6 +34,16 @@ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
controller: Controller = None
if __name__ != "__main__":
# Imported by a WSGI server rather than run as a script, so main()'s
# argparse never executes -- build the one controller instance here
# instead. HEOS_DEMO lets the fake-speakers mode work this way too.
if os.environ.get("HEOS_DEMO", "").lower() in ("1", "true", "yes"):
from demo import DemoController
controller = DemoController(config)
else:
controller = Controller(config)
def handle_errors(view):
"""One place to turn our three failure modes into sensible JSON."""
@@ -39,7 +53,7 @@ def handle_errors(view):
return view(*args, **kwargs)
except (TargetError, ValueError) as exc:
return jsonify({"error": str(exc)}), 400
except (HeosError, AvrError) as exc:
except HeosError as exc:
return jsonify({"error": str(exc)}), 502
return wrapped
@@ -149,6 +163,19 @@ def api_playback():
return jsonify({"target": key, "state": controller.toggle_play(key, state)})
@app.post("/api/skip")
@handle_errors
def api_skip():
"""Jump to the next (or previous) track in a room's queue."""
data = _payload()
key = _target_from(data)
direction = data.get("direction", "next")
if direction not in ("next", "previous"):
raise ValueError("'direction' must be next or previous")
controller.skip(key, direction)
return jsonify({"target": key, "direction": direction})
@app.post("/api/group")
@handle_errors
def api_group():
@@ -174,8 +201,7 @@ def api_group_none():
@app.get("/api/avr/inputs")
@handle_errors
def api_avr_inputs():
refresh = request.args.get("refresh") in ("1", "true", "yes")
return jsonify(controller.avr.inputs(refresh=refresh))
return jsonify(controller.avr_inputs())
@app.post("/api/avr/input")
@@ -183,8 +209,8 @@ def api_avr_inputs():
def api_avr_set_input():
code = _payload().get("code")
if not code:
raise ValueError("Provide 'code', e.g. GAME -- see GET /api/avr/inputs")
return jsonify(controller.avr.select_input(code))
raise ValueError("Provide 'code', e.g. inputs/aux_in_1 -- see GET /api/avr/inputs")
return jsonify(controller.avr_select_input(code))
# --- The original bridge's API, unchanged -----------------------------
@@ -311,30 +337,21 @@ def legacy_raw(subpath):
return jsonify(controller.heos.command(subpath, **request.args.to_dict()))
@app.get("/avr/raw")
@handle_errors
def legacy_avr_raw():
cmd = request.args.get("cmd")
if not cmd:
raise ValueError("provide '?cmd=<raw telnet command>'")
return jsonify({"lines": controller.avr.telnet.request(cmd, timeout=3.0)})
@app.get("/avr/inputs")
@handle_errors
def legacy_avr_inputs():
return jsonify(controller.avr.inputs(refresh=True))
return jsonify(controller.avr_inputs())
@app.route("/avr/input", methods=["GET", "POST"])
@handle_errors
def legacy_avr_input():
if request.method == "GET":
return jsonify(controller.avr.current_input() or {"error": "AVR not reachable"})
return jsonify(controller.avr_current_input() or {"error": "AVR not reachable"})
code = request.args.get("input")
if not code:
raise ValueError("provide '?input=<code>', e.g. GAME, TV, CD, AUX1")
return jsonify(controller.avr.select_input(code))
raise ValueError("provide '?input=<code>', e.g. inputs/aux_in_1 -- see GET /avr/inputs")
return jsonify(controller.avr_select_input(code))
def main():