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