This commit is contained in:
@@ -1,163 +1,33 @@
|
||||
# HEOS panel
|
||||
# Heos app
|
||||
|
||||
A phone-sized web remote for a Denon HEOS system, meant to be added to the
|
||||
iOS home screen and used instead of the HEOS app. One Flask process serves
|
||||
both the interface and the HTTP bridge behind it.
|
||||
_The default HEOS app is so bad I had to make one myself._
|
||||
|
||||
Everything it does fits on one screen:
|
||||
A phone-sized web remote for a multi-room HEOS system, meant to be added to the iOS home screen and used instead of the HEOS app. One Flask process serves both the interface and the HTTP bridge behind it.
|
||||
|
||||
- **Volume** up/down for the Home 400 and the Living Room pair. A tap lands
|
||||
on the next multiple of `VOLUME_STEP` — from 23 it goes to 25, not 28 —
|
||||
so the levels stay round. Hold to keep moving, or drag the level along
|
||||
its bar to set it outright.
|
||||
- **Play or pause** either room, or skip a track, while it is playing
|
||||
Spotify — the buttons only show up then, since an AVR input has nothing
|
||||
to pause or skip. A room that is grouped shares the AVR's
|
||||
transport, so pausing it pauses the group — HEOS's doing, not the panel's:
|
||||
a group has one thing playing, by definition
|
||||
- **Group** either room with the AVR — the AVR is always the host, so its
|
||||
sound takes over whatever joins it. A room that joins moves *into* the
|
||||
Home Cinema card, so one glance says what is playing together
|
||||
Everything fits on one screen:
|
||||
|
||||
- **Volume** up/down for each room. A tap lands on the next multiple of `VOLUME_STEP` — from 23 it goes to 25, not 28 — so the levels stay round. Hold to keep moving, or drag the level along its bar to set it outright.
|
||||
- **Navigate** either room (play / pause / prev / next), while it is playing Spotify
|
||||
- **Group** either room with the AVR. the AVR is always the host, so its sound takes over whatever joins it. A room that joins moves _into_ the Home Cinema card, so one glance says what is playing together
|
||||
- **Ungroup** either room again, or all of them at once
|
||||
- **Change the AVR's input**, listed under the names you gave them, minus
|
||||
the sources you deleted in the AVR's setup menu
|
||||
- **Resume Spotify** on a room from Fifou's or Clarita's account, one
|
||||
button each — the reverse of connecting to it from the Spotify app.
|
||||
Optional; see [Spotify](#spotify) below
|
||||
- **Change the AVR's input**, listed under the names you gave them, minus the sources you deleted in the AVR's setup menu
|
||||
- **Resume Spotify** on a room from either account (the reverse of connecting to it from the Spotify app) — labeled with the familiar names you gave them, e.g. Fifou's or Clarita's. Optional, see [Spotify](#configure-spotify-optional) below
|
||||
|
||||
## The kit it assumes
|
||||
|
||||
| Room | Device | How HEOS addresses it |
|
||||
| --- | --- | --- |
|
||||
| Living Room | 2× Denon Home 200 as an In-Room Group | a **group** (`gid`) |
|
||||
| Living Room | 2× Denon Home 200 as an In-Room Group | a **player** (`pid`) -- HEOS pairs them at the hardware level |
|
||||
| Lego Room | Denon Home 400 | a **player** (`pid`) |
|
||||
| Home Cinema | Denon AVR-X3800H | a **player** |
|
||||
|
||||
Any other mix works — it is all in `config.py`.
|
||||
See `config.json.example`.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
cd /var/www/html/heos
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python3 app.py
|
||||
```
|
||||
### CI/CD
|
||||
|
||||
Then open `http://<pi-ip>:5443/`.
|
||||
|
||||
The virtual environment is not optional on a current Raspberry Pi OS:
|
||||
`pip install` straight into the system Python is refused there with
|
||||
`externally-managed-environment`. It also keeps Flask out of the way of
|
||||
anything else running on the Pi.
|
||||
|
||||
Every later `python3 ...` command here assumes the environment is active
|
||||
(`source .venv/bin/activate`); `deactivate` when you are done. The service
|
||||
below calls the environment's Python directly, so it does not care.
|
||||
|
||||
## Configure
|
||||
|
||||
Open `http://<pi-ip>:5443/api/targets` and copy the exact `name` HEOS reports
|
||||
for each device into `TARGETS` in `config.py`. The names come from whatever
|
||||
you typed in the HEOS app, so they rarely match the model names.
|
||||
|
||||
```python
|
||||
TARGETS = {
|
||||
"avr": {"label": "Home Cinema", "heos_name": "Home Cinema"},
|
||||
"home400": {"label": "Lego Room", "heos_name": "Lego Room"},
|
||||
"living_room_group": {"label": "Living Room", "heos_name": "Denon Home 200 L"},
|
||||
}
|
||||
HOST_KEY = "avr" # always the group host
|
||||
ROOM_KEYS = ["home400", "living_room_group"] # the cards, in order
|
||||
```
|
||||
|
||||
`HEOS_HOST` only needs to point at **one** device: HEOS is distributed, so any
|
||||
unit can see and control the whole network — including the AVR's own inputs,
|
||||
so there is nothing AVR-specific to configure beyond its entry in `TARGETS`.
|
||||
|
||||
`VOLUME_STEP` is the grid the volume buttons snap to, not simply how much
|
||||
they add: at 5, a tap moves 23 to 25 and 25 to 30.
|
||||
|
||||
`AVR_INPUT_CODES` narrows the input picker to the sources you actually use,
|
||||
by their HEOS input id (`GET /api/avr/inputs` shows the exact strings, e.g.
|
||||
`inputs/aux_in_1`), and sets their order; leave it empty to list everything
|
||||
HEOS reports for it.
|
||||
|
||||
## Spotify
|
||||
|
||||
Spotify dropped native, browsable HEOS integration years ago — it is
|
||||
Connect-only on HEOS now, and Connect only works phone → speaker: the
|
||||
Spotify app pushes playback to a room, and there is nothing in HEOS that
|
||||
asks for the reverse. So "Resume Spotify" doesn't go through HEOS at all;
|
||||
it calls Spotify's own Web API to transfer the account's current playback
|
||||
onto the room's Spotify Connect receiver, which the receiver already
|
||||
advertises on the LAN whether or not anything is playing. This needs
|
||||
Spotify Premium and a one-time login, since Spotify has no way to grant
|
||||
that without a human approving it once.
|
||||
|
||||
1. Create an app at the
|
||||
[Spotify Developer dashboard](https://developer.spotify.com/dashboard)
|
||||
(any name), and add this Redirect URI in its settings:
|
||||
`http://127.0.0.1:8899/callback`. Spotify allows plain `http` for a
|
||||
`127.0.0.1` redirect specifically, which is why the login below needs no
|
||||
HTTPS setup.
|
||||
2. While the app is in development mode, Spotify only lets accounts you
|
||||
have listed log in to it: add both accounts' email addresses under the
|
||||
app's **User Management**.
|
||||
3. Run the one-time login once per account, from a machine with a browser
|
||||
(your laptop is fine — it doesn't have to be the Pi):
|
||||
```bash
|
||||
python3 tools/spotify_auth.py --client-id <id> --client-secret <secret> --account fifou
|
||||
python3 tools/spotify_auth.py --client-id <id> --client-secret <secret> --account clarita
|
||||
```
|
||||
Log in as that account each time — use a private window for the second
|
||||
run, or Spotify just approves whichever account the browser is already
|
||||
logged in as. Each run prints `SPOTIFY_CLIENT_ID` and
|
||||
`SPOTIFY_CLIENT_SECRET` (the same both times) plus that account's own
|
||||
`SPOTIFY_FIFOU_REFRESH_TOKEN` or `SPOTIFY_CLARITA_REFRESH_TOKEN`.
|
||||
4. Put those four lines in a `.env` file in this directory (already
|
||||
git-ignored, and excluded from the deploy rsync the same way
|
||||
`members.json` is — see "What survives a deploy" below). `config.py`
|
||||
reads it directly, so no shell-sourcing step or restart-in-the-right-
|
||||
terminal gotcha — just `python3 app.py` as normal. A real exported
|
||||
environment variable (or systemd's `EnvironmentFile`, in the service)
|
||||
still overrides the file rather than the other way round.
|
||||
|
||||
`config.py` never sees the credentials themselves — it only reads them out
|
||||
of the environment. Each speaker's card then gets one Spotify button per
|
||||
account in `SPOTIFY_ACCOUNTS` that has a refresh token; an account without
|
||||
one simply gets no button. The AVR's card has none. Whichever account is
|
||||
playing on a room right now gets a border around its button — the panel
|
||||
asks Spotify, but only while HEOS says a room is on Spotify. Each room resumes on the Spotify Connect device named
|
||||
`spotify_name` in its `TARGETS` entry, falling back to `heos_name` if that
|
||||
key is missing (they usually match). `GET /api/spotify/devices?account=fifou`
|
||||
lists what Spotify actually calls each device, if a room's button ever says
|
||||
one isn't visible.
|
||||
|
||||
A room's previous, play/pause and next buttons only appear while HEOS
|
||||
reports it is playing (or paused on) Spotify, and one of your `SPOTIFY_ACCOUNTS` is the
|
||||
one playing it — the same match that borders its button. To see what HEOS reports for a room, use
|
||||
`GET /raw/player/get_now_playing_media?pid=<pid>` with a pid from
|
||||
`/api/targets`: Spotify shows up as `"sid": 4`. The song, artist and cover
|
||||
under each room's name come from that same reply (`song`, `artist`,
|
||||
`image_url`), whichever of them HEOS fills in. They stay while paused and go
|
||||
once the room stops. An AVR input shows none, since its "song" is just the
|
||||
input's name.
|
||||
|
||||
## Add it to the iOS home screen
|
||||
|
||||
Open the page in Safari → Share → **Add to Home Screen**. It then launches
|
||||
full-screen with no browser chrome, which is the point of the exercise.
|
||||
|
||||
Safari will only offer that over plain HTTP on the LAN, which is fine here;
|
||||
if you ever put it behind a domain name, give it HTTPS.
|
||||
|
||||
## Run it as a service
|
||||
|
||||
`deploy/heos-panel.service` runs the panel out of its own virtualenv, under
|
||||
`gunicorn` rather than `python3 app.py`'s dev server, and restarts it if it
|
||||
dies:
|
||||
`deploy/heos-panel.service` runs the panel out of its own virtualenv, under `gunicorn` rather than `python3 app.py`'s dev server, and restarts it if it dies.
|
||||
|
||||
```bash
|
||||
sudo cp deploy/heos-panel.service /etc/systemd/system/
|
||||
@@ -167,72 +37,34 @@ sudo systemctl enable --now heos-panel
|
||||
|
||||
Edit `User=` and the paths in it if you keep the panel somewhere else.
|
||||
|
||||
`python3 app.py` (no gunicorn) is still the right way to run it by hand
|
||||
while working on it — see `--demo` below — the dev-server warning it prints
|
||||
is expected there and only matters for the service above.
|
||||
|
||||
## Deploying from Gitea
|
||||
|
||||
`.gitea/workflows/deploy.yml` checks out the push, runs the tests, rsyncs
|
||||
the tree into place, installs anything new from `requirements.txt`,
|
||||
restarts the service and waits for the panel to answer again. The tests run
|
||||
before the rsync, so a failure leaves the server exactly as it was.
|
||||
|
||||
### The runner
|
||||
|
||||
Host mode, on the machine that serves the panel, registered with the label
|
||||
`heos` (`runs-on:` must match, or the job queues forever), running as a user
|
||||
that can write to the deploy path. It also needs **node 20 or newer** on its
|
||||
PATH: `actions/checkout` is a JavaScript action, and a host-mode runner has
|
||||
nothing else to run one with — without it the job fails immediately with
|
||||
`Cannot find: node in PATH`.
|
||||
|
||||
### Once, on the server
|
||||
|
||||
The deploy path does not need to be a checkout — an empty directory the
|
||||
runner can write to is enough:
|
||||
Make sure the CI/CD's \<user\> can restart the service:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/html/heos
|
||||
sudo chown "$(id -un)": /var/www/html/heos
|
||||
|
||||
echo "$(id -un) ALL=(ALL) NOPASSWD: $(command -v systemctl) restart heos-panel" \
|
||||
echo "<user> ALL=(ALL) NOPASSWD: $(command -v systemctl) restart heos-panel" \
|
||||
| sudo tee /etc/sudoers.d/heos-panel
|
||||
sudo chmod 440 /etc/sudoers.d/heos-panel
|
||||
```
|
||||
|
||||
Run those **as the user the runner runs as**, not as yourself — the first
|
||||
step of the workflow prints who that is. Two things in that sudoers line are
|
||||
easy to get wrong, and the workflow checks both before it deploys anything,
|
||||
printing the line back at you with the right values filled in:
|
||||
Run the Gitea CI/CD.
|
||||
|
||||
- the user has to be the runner's, and
|
||||
- the path has to be the one `sudo` resolves from `PATH`. It compares that
|
||||
string against the sudoers line without following symlinks, so on a system
|
||||
where `/bin` links to `/usr/bin` the two spellings are not interchangeable.
|
||||
### Manually
|
||||
|
||||
Then push. That first run deploys the files and builds the virtualenv, and
|
||||
stops at the restart, because the service does not exist yet. Install it
|
||||
from the copy it just put there:
|
||||
Clone repo to `/var/www/html/heos` and:
|
||||
|
||||
```bash
|
||||
sudo cp /var/www/html/heos/deploy/heos-panel.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable heos-panel
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python3 app.py
|
||||
```
|
||||
|
||||
Edit `User=` and the paths in the unit first if the panel lives somewhere
|
||||
else or runs as someone else. Re-run the workflow and it goes green.
|
||||
### Add a reverse proxy (optional)
|
||||
|
||||
### What survives a deploy
|
||||
It serves the panel at `/heos` and refuses everything else on that host; the file ends with the two-line change that puts it at the root instead.
|
||||
|
||||
The rsync excludes `.venv`, `members.json` and `.env`, so the runtime, the
|
||||
stereo pair's learned membership, and Spotify's credentials are left alone by
|
||||
`--delete`. Everything else in the deploy path is made to match the repo,
|
||||
`config.py` included: your device names live in git, so change them there and
|
||||
push rather than editing the deployed copy.
|
||||
Both ship restricted to the local network — this controls the speakers, and it usually hangs off a host with a public certificate. Delete the `RequireAny` block (Apache) or the `allow`/`deny` lines (nginx) to open it up.
|
||||
|
||||
## Behind a reverse proxy, at /heos
|
||||
#### Apache
|
||||
|
||||
`deploy/heos.apache.conf` reverse-proxies `/heos` to the panel with Apache:
|
||||
|
||||
@@ -243,142 +75,82 @@ sudo a2enconf heos
|
||||
sudo apachectl configtest && sudo systemctl reload apache2
|
||||
```
|
||||
|
||||
`deploy/heos.nginx.conf` is the same thing for nginx — a whole `server`
|
||||
block for `rpioffice.nest.domain.com`, ready for `sites-available`:
|
||||
#### Nginx
|
||||
|
||||
`deploy/heos.nginx.conf` reverse-proxies `/heos` to the panel with Nginx:
|
||||
|
||||
```bash
|
||||
sudo cp deploy/heos.nginx.conf /etc/nginx/sites-available/heos
|
||||
sudo ln -s /etc/nginx/sites-available/heos /etc/nginx/sites-enabled/heos
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
sudo certbot --nginx -d rpioffice.nest.domain.com # optional, adds the 443 block
|
||||
```
|
||||
|
||||
It serves the panel at `/heos` and refuses everything else on that host;
|
||||
the file ends with the two-line change that puts it at the root instead.
|
||||
## Configure
|
||||
|
||||
Both ship restricted to the local network — this controls the speakers, and
|
||||
it usually hangs off a host with a public certificate. Delete the
|
||||
`RequireAny` block (Apache) or the `allow`/`deny` lines (nginx) to open it
|
||||
up.
|
||||
Copy `config.json.example` to `config.json` and fill in the exact `name` HEOS reports for each device. The names come from whatever you typed in the HEOS app, so they rarely match the model names.
|
||||
|
||||
The app works at either address without being told which. The proxy sends
|
||||
`X-Forwarded-Prefix: /heos`, and every URL the app generates — stylesheet,
|
||||
icons, the manifest's `start_url`, every `fetch` — picks up that prefix.
|
||||
Serve it straight from port 5443 and the same URLs come out as `/...`.
|
||||
That header is what the `headers` module is for; without it the page loads
|
||||
and nothing on it works.
|
||||
`web_port` is this app listening port.
|
||||
|
||||
Two things worth knowing:
|
||||
Open `http://<host-ip>:<web_port>/api/targets`.
|
||||
|
||||
- The proxy block takes `/heos` away from the filesystem, so the source
|
||||
under `/var/www/html/heos` stops being served as static files.
|
||||
- The panel still answers directly on `<pi-ip>:5443`. Start it with
|
||||
`--host 127.0.0.1` if you want Apache to be the only way in.
|
||||
`host_key` is the group host into which the room will be grouped into (the AVR), and `room_keys` sets which rooms can be grouped (also set the card order on the app).
|
||||
|
||||
## How the grouping actually works
|
||||
`heos_host` only needs to point at **one** device: HEOS is distributed, so any unit can see and control the whole network. `heos_port`, `zidoo_host` and `zidoo_port` all work the same way.
|
||||
|
||||
Worth knowing, because HEOS makes two things easy to get wrong.
|
||||
`volume_step` is the grid the volume buttons snap to: If set to `5`, a tap moves `23` to `25` and `25` to `30`.
|
||||
|
||||
**`set_group` replaces a group wholesale.** There is no "add this player".
|
||||
Joining a second room therefore re-sends every member of the group, and the
|
||||
host's `pid` has to come first — that is what makes the AVR the leader whose
|
||||
content everyone plays.
|
||||
Restart app flask/gunicorn.
|
||||
|
||||
**Your Home 200 pair is a group, not a speaker.** Merging it into the AVR
|
||||
means sending *both* speakers' pids; sending only the leader would leave the
|
||||
second Home 200 playing on its own. And once merged, the pair's own `gid`
|
||||
stops existing, so:
|
||||
## Configure Spotify (Optional)
|
||||
|
||||
- unmerging re-issues `set_group` with the pair's two pids, rebuilding it
|
||||
- volume falls back to setting both players directly, since there is no
|
||||
group volume to set any more
|
||||
Spotify dropped native, browsable HEOS integration years ago — it is Connect-only on HEOS now, and Connect only works phone → speaker: the Spotify app pushes playback to a room, and there is nothing in HEOS that asks for the reverse. So "Resume Spotify" doesn't go through HEOS at all; it calls Spotify's own Web API to transfer the account's current playback onto the room's Spotify Connect receiver, which the receiver already advertises on the LAN whether or not anything is playing.
|
||||
|
||||
The panel learns the pair's members the first time it sees them un-merged and
|
||||
remembers them in `members.json`, which is what lets it rebuild the pair after
|
||||
a restart. If you would rather pin them down, list them in `config.py`:
|
||||
This needs Spotify Premium and a one-time login, since Spotify has no way to grant that without a human approving it once.
|
||||
|
||||
```python
|
||||
"living_room_group": {
|
||||
"label": "Living Room",
|
||||
"heos_name": "Denon Home 200 L",
|
||||
"players": ["Denon Home 200 L", "Denon Home 200 R"], # leader first
|
||||
},
|
||||
```
|
||||
1. Create an app at the [Spotify Developer dashboard](https://developer.spotify.com/dashboard) (any name), and add this Redirect URI in its settings: `http://127.0.0.1:8899/callback`. Spotify allows plain `http` for a `127.0.0.1` redirect specifically, which is why the login below needs no HTTPS setup.
|
||||
2. While the app is in development mode, Spotify only lets accounts you have listed log in to it: add both accounts' email addresses under the app's **User Management**.
|
||||
3. Run the one-time login once per account, from a machine with a browser (your laptop is fine — it doesn't have to be the Pi):
|
||||
|
||||
Leaving a room deliberately does *not* rewrite the AVR's group, so the other
|
||||
room's music does not restart.
|
||||
```bash
|
||||
python3 tools/spotify_auth.py --client-id <id> --client-secret <secret> --account 1
|
||||
python3 tools/spotify_auth.py --client-id <id> --client-secret <secret> --account 2
|
||||
```
|
||||
|
||||
**A newly joined room can stay silent on the AVR's input.** Joining alone
|
||||
does not push audio to it — HEOS needs telling *again* which input is
|
||||
playing before it streams that input to the new member, the same reselect
|
||||
you'd otherwise do by hand in the HEOS app (Home → Sources → AV). `join()`
|
||||
does this for you: it reads the AVR's current input back and replays it
|
||||
through `browse/play_input` right after the group merge.
|
||||
Log in as that account each time — use a private window for the second run, or Spotify just approves whichever account the browser is already logged in as. Each run prints `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` (the same both times) plus that account's own `SPOTIFY_ACCOUNT1_REFRESH_TOKEN` or `SPOTIFY_ACCOUNT2_REFRESH_TOKEN`.
|
||||
4. Put those four lines in a `.env` file in this directory
|
||||
5. Restart app flask/gunicorn
|
||||
|
||||
## One protocol, not two
|
||||
A room's previous, play/pause and next buttons only appear while HEOS reports it is playing (or paused on) Spotify, and one of your `SPOTIFY_ACCOUNTS` is the one playing it — the same match that borders its button. To see what HEOS reports for a room, use `GET /raw/player/get_now_playing_media?pid=<pid>` with a pid from `/api/targets`: Spotify shows up as `"sid": 4`. The song, artist and cover under each room's name come from that same reply (`song`, `artist`, `image_url`), whichever of them HEOS fills in. They stay while paused and go once the room stops. An AVR input shows none, since its "song" is just the input's name.
|
||||
|
||||
Everything goes over the HEOS CLI (port 1255) — players, groups, volume, and
|
||||
the AVR's own inputs. `browse/browse` on the AVR's pid lists its inputs under
|
||||
whatever names you gave them in its setup menu; HEOS reports those renamed
|
||||
labels itself, so there used to be a second client here for the AVR's Denon
|
||||
Telnet port just to fetch them, and it is not needed any more.
|
||||
## Add it to the iOS home screen
|
||||
|
||||
Selecting an input goes through `browse/play_input`, not a raw `SI<code>`
|
||||
Telnet command, for the same reason joining a room re-sends it (see above):
|
||||
that is what actually tells HEOS to *stream* the input to whichever players
|
||||
are grouped with the AVR, not just which jack the AVR itself is listening to.
|
||||
Open the page in Safari → Share → **Add to Home Screen**. It then launches full-screen with no browser chrome, which is the point of the exercise.
|
||||
|
||||
Safari will only offer that over plain HTTP on the LAN, which is fine here; if you ever put it behind a domain name, give it HTTPS.
|
||||
|
||||
## HTTP API
|
||||
|
||||
Used by the interface:
|
||||
|
||||
| | |
|
||||
| HTTP Call | Description |
|
||||
| --- | --- |
|
||||
| `GET /api/state` | everything the UI draws, in one call |
|
||||
| `GET /api/targets` | every player and group HEOS can see |
|
||||
| `POST /api/volume` | `{"target": "home400", "steps": 1}` — taps, snapped to `VOLUME_STEP`. Also takes `delta` (raw points) or `level` (absolute) |
|
||||
| `POST /api/mute` | `{"target": "home400"}` |
|
||||
| `POST /api/playback` | `{"target": "home400", "state": "pause"}`, or no `state` to toggle |
|
||||
| `POST /api/skip` | `{"target": "home400", "direction": "next"}` — `previous` too |
|
||||
| `POST /api/group` | `{"target": "home400", "joined": true}` |
|
||||
| `POST /api/volume` | `{"target": "lego_room", "steps": 1}` — taps, snapped to `VOLUME_STEP`. Also takes `delta` (raw points) or `level` (absolute) |
|
||||
| `POST /api/mute` | `{"target": "lego_room"}` |
|
||||
| `POST /api/playback` | `{"target": "lego_room", "state": "pause"}`, or no `state` to toggle |
|
||||
| `POST /api/skip` | `{"target": "lego_room", "direction": "next"}` — `previous` too |
|
||||
| `POST /api/group` | `{"target": "lego_room", "joined": true}` |
|
||||
| `POST /api/group/none` | every room back on its own |
|
||||
| `GET /api/avr/inputs` | your renamed sources, over HEOS |
|
||||
| `POST /api/avr/input` | `{"code": "inputs/aux_in_1"}` |
|
||||
| `GET /api/spotify/devices?account=fifou` | every Spotify Connect receiver that account currently sees (needs [Spotify](#spotify) configured) |
|
||||
| `POST /api/spotify/resume` | `{"target": "home400", "account": "fifou"}` — transfers that account's current playback there and resumes it |
|
||||
| `GET /api/spotify/devices?account=account1` | every Spotify Connect receiver that account currently sees (needs [Spotify](#configure-spotify-optional) configured) |
|
||||
| `POST /api/spotify/resume` | `{"target": "lego_room", "account": "account1"}` — transfers that account's current playback there and resumes it |
|
||||
|
||||
`POST /volume/up` and `/volume/down` take one snapped tap by default; pass
|
||||
`?step=3` and they move that many raw points instead, as they always did.
|
||||
|
||||
The original bridge's endpoints still answer, so existing Shortcuts and
|
||||
scripts keep working: `/targets`, `/volume`, `/volume/{set,up,down,mute}`,
|
||||
`/playback/{play,pause,stop,next,previous}`, `/group/{create,remove}`,
|
||||
`/inputs`, `/input/{set,relay}`, `/avr/{input,inputs}`, `/raw/<command>`.
|
||||
|
||||
Worth keeping for troubleshooting:
|
||||
|
||||
```
|
||||
GET /raw/browse/browse?sid=1027 # any heos:// command, raw reply
|
||||
```
|
||||
|
||||
## Working on it
|
||||
## Demo/Tests
|
||||
|
||||
```bash
|
||||
python3 app.py --demo # fake speakers, real interface
|
||||
python3 -m unittest discover -s tests -t . # runs against a fake HEOS network
|
||||
python3 tools/make_icons.py # re-render the icons from static/logo.svg
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
app.py Flask: the UI, the API, and the old bridge's routes
|
||||
controller.py what a room is, what grouping means, volume, the AVR's inputs
|
||||
heos.py HEOS CLI client (persistent socket, reconnects itself)
|
||||
spotify.py Spotify Web API client, for the "Resume Spotify" button
|
||||
config.py your devices and preferences
|
||||
demo.py fake speakers for --demo
|
||||
templates/ static/ the interface
|
||||
tests/ fake HEOS and Spotify servers, and tests against them
|
||||
tools/spotify_auth.py one-time Spotify login, prints the refresh token
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user