Set hard-coded rooms order in config file
Deploy HEOS panel / deploy (push) Failing after 49s

This commit is contained in:
franzz committed 2026-09-22 15:37:07 +02:00
1 parent 83a26c9476
commit 0c8672789d
11 files changed
+111 -120

No files matched your search

+3 -3
View File
@@ -91,11 +91,11 @@ sudo nginx -t && sudo systemctl reload nginx
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.
`web_port` is this app listening port.
`app_port` is this app listening port.
Open `http://<host-ip>:<web_port>/api/targets`.
Open `http://<host-ip>:<app_port>/api/targets`.
`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).
`host_room` is the group host into which the room will be grouped into (the AVR), and `guest_rooms` sets which rooms can be grouped. Each room in `rooms` (the host included) also needs an `order` -- the card's position on the app, lowest first.
`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.
+17 -16
View File
@@ -44,14 +44,14 @@ spotify: dict = {} # account key -> SpotifyClient, configured accounts only
# controller.py's module docstring), so its API has no field that says how
# many units heos_name actually names -- that has to come from config.json's
# "in_room_group" instead (config.IN_ROOM_GROUPS). The AVR/speaker split
# needs no such flag: this app's own design always makes HOST_KEY the AVR
# and ROOM_KEYS plain speakers, so it is known before any HEOS call is made.
# needs no such flag: this app's own design always makes HOST_ROOM the AVR
# and GUEST_ROOMS plain speakers, so it is known before any HEOS call is made.
#
# "kind" is passed straight through to the template as-is (one of
# config.IN_ROOM_GROUPS, or "avr") -- which icon that draws is the
# interface's business, not ours: see ICONS in static/app.js.
def _room_meta(key: str, kind: str) -> dict:
return {"key": key, "kind": kind, **config.TARGETS[key]}
return {"key": key, "kind": kind, **config.ROOMS[key]}
def _build_spotify() -> dict:
@@ -104,9 +104,9 @@ def _payload() -> dict:
def _target_from(data: dict, field: str = "target") -> str:
key = data.get(field)
if not key:
raise ValueError(f"Missing '{field}'. Valid rooms: {', '.join(config.TARGETS)}")
if key not in config.TARGETS:
raise ValueError(f"Unknown room '{key}'. Valid rooms: {', '.join(config.TARGETS)}")
raise ValueError(f"Missing '{field}'. Valid rooms: {', '.join(config.ROOMS)}")
if key not in config.ROOMS:
raise ValueError(f"Unknown room '{key}'. Valid rooms: {', '.join(config.ROOMS)}")
return key
@@ -122,7 +122,7 @@ def _spotify_from(data: dict) -> SpotifyClient:
def _spotify_name(key: str) -> str:
target = config.TARGETS[key]
target = config.ROOMS[key]
return target.get("spotify_name", target["heos_name"])
@@ -234,7 +234,7 @@ def _release_over_heos(key: str):
inputs = controller.heos_inputs(key)
if not inputs:
raise ValueError(
f"'{config.TARGETS[key]['label']}' did not answer on the network, and HEOS lists "
f"'{config.ROOMS[key]['label']}' did not answer on the network, and HEOS lists "
"no input on it to fall back to -- so there is no way from here to take this room "
"off Spotify.")
# An AUX jack with nothing in it is the quietest thing a speaker can be
@@ -329,14 +329,14 @@ def _link_zidoo_poster(avr: dict):
# --- The UI -----------------------------------------------------------
@app.route("/")
def index():
host = _room_meta(config.HOST_ROOM, "avr")
rooms = {config.HOST_ROOM: host, **{key: _room_meta(key, config.ROOMS[key].get("in_room_group", "none")) for key in config.GUEST_ROOMS}}
return render_template(
"index.html",
app_name=config.APP_NAME,
host=_room_meta(config.HOST_KEY, "avr"),
rooms=[
_room_meta(key, config.TARGETS[key].get("in_room_group", "none"))
for key in config.ROOM_KEYS
],
version=config.APP_VERSION,
host=host,
rooms=sorted(rooms.values(), key=lambda room: room["order"]),
step=config.VOLUME_STEP,
spotify_accounts=[{"key": key, "label": config.SPOTIFY_ACCOUNTS[key]["label"]} for key in spotify],
)
@@ -360,6 +360,7 @@ def api_state():
if not data.get("demo"):
_mark_spotify_accounts(data["rooms"])
_link_zidoo_poster(data["avr"])
data["version"] = config.APP_VERSION
return jsonify(data)
@@ -433,7 +434,7 @@ def api_playback():
state = data.get("state")
if state is not None and state not in ("play", "pause", "stop"):
raise ValueError("'state' must be play, pause or stop -- or left out to toggle")
if key == config.HOST_KEY:
if key == config.HOST_ROOM:
if state == "stop":
raise ValueError("The AVR's card plays a device on one of its inputs, which can only "
"play or pause -- 'stop' is for a HEOS room")
@@ -465,7 +466,7 @@ def api_seek():
if data.get("position_ms") is None:
raise ValueError("Provide 'position_ms', where to jump to")
position = max(0, int(data["position_ms"]))
if key == config.HOST_KEY:
if key == config.HOST_ROOM:
controller.zidoo_seek(position)
else:
account = _spotify_playing_on().get(_spotify_name(key))
@@ -711,7 +712,7 @@ def legacy_avr_input():
def main():
global controller, spotify
parser = argparse.ArgumentParser(description="HEOS panel")
parser.add_argument("--port", type=int, default=config.WEB_PORT)
parser.add_argument("--port", type=int, default=config.APP_PORT)
parser.add_argument("--host", default="0.0.0.0",
help="0.0.0.0 so your phone can reach it directly; "
"127.0.0.1 to allow only a local reverse proxy")
+16 -13
View File
@@ -1,27 +1,30 @@
{
"web_port": 5443,
"app_port": 5443,
"heos_host": "REPLACE_WITH_HEOS_HOST",
"heos_port": 1255,
"zidoo_host": "REPLACE_WITH_ZIDOO_HOST",
"zidoo_port": 9529,
"targets": {
"avr": {
"label": "Home Cinema",
"heos_name": "REPLACE_WITH_EXACT_HEOS_NAME"
},
"lego_room": {
"label": "Lego Room",
"heos_name": "REPLACE_WITH_EXACT_HEOS_NAME"
},
"rooms": {
"living_room": {
"label": "Living Room",
"heos_name": "REPLACE_WITH_EXACT_HEOS_NAME",
"spotify_name": "REPLACE_ONLY_IF_DIFFERENT_FROM_HEOS_NAME",
"in_room_group": "stereo-pair"
"in_room_group": "stereo-pair",
"order": 1
},
"avr": {
"label": "Home Cinema",
"heos_name": "REPLACE_WITH_EXACT_HEOS_NAME",
"order": 2
},
"lego_room": {
"label": "Lego Room",
"heos_name": "REPLACE_WITH_EXACT_HEOS_NAME",
"order": 3
}
},
"host_key": "avr",
"room_keys": ["living_room", "lego_room"],
"host_room": "avr",
"guest_rooms": ["living_room", "lego_room"],
"volume_step": 5,
"spotify_accounts": {
"account1": "REPLACE_WITH_ACCOUNT_1_NAME",
+27 -7
View File
@@ -7,6 +7,7 @@ reports for your own devices.
import json
import os
import subprocess
from pathlib import Path
@@ -58,29 +59,31 @@ except FileNotFoundError:
"and fill in your own rooms (see the README's Configure section)."
)
TARGETS = _cfg["targets"]
ROOMS = _cfg["rooms"]
for _key, _target in TARGETS.items():
_group = _target.get("in_room_group", "none")
for _index, (_key, _room) in enumerate(ROOMS.items()):
_group = _room.get("in_room_group", "none")
if _group not in IN_ROOM_GROUPS:
raise SystemExit(
f"{_config_path}: '{_key}' has in_room_group '{_group}', not one "
f"of {', '.join(IN_ROOM_GROUPS)}"
)
# No 'order' in config.json just means "wherever it falls in rooms".
_room.setdefault("order", _index)
# The AVR is always the group host: its content takes over every room
# that joins, which is the whole point of the merge buttons.
HOST_KEY = _cfg["host_key"]
HOST_ROOM = _cfg["host_room"]
# The rooms that get a card with volume + a join/leave button, in order.
ROOM_KEYS = _cfg["room_keys"]
# The rooms that get a card with volume + a join/leave button.
GUEST_ROOMS = _cfg["guest_rooms"]
# The grid the volume buttons snap to. A tap moves to the next multiple of
# this rather than adding it, so at 5 a level of 23 goes to 25, not 28.
VOLUME_STEP = _cfg["volume_step"]
# Port the panel itself listens on.
WEB_PORT = _cfg["web_port"]
APP_PORT = _cfg["app_port"]
# --- Network ----------------------------------------------------------
# Any ONE HEOS device's IP is enough: HEOS is a distributed system, so
@@ -104,6 +107,23 @@ ZIDOO_INPUT_CODE = "inputs/mediaplayer" # see GET /api/avr/inputs
# Shown as the app's name on the iOS home screen.
APP_NAME = "Heos"
# The deployed commit, so the UI can tell when the Pi has moved on to a
# newer version and reload itself -- iOS otherwise resumes a suspended
# home-screen session without ever checking back with the server. Empty
# when git isn't available; app.js treats that as "nothing to compare".
def _app_version() -> str:
try:
result = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
cwd=Path(__file__).resolve().parent,
capture_output=True, text=True, timeout=1,
)
return result.stdout.strip() if result.returncode == 0 else ""
except OSError:
return ""
APP_VERSION = _app_version()
# --- Spotify (optional) -------------------------------------------------
# One Spotify button per account on each speaker's card (not the AVR's),
# resuming that account's playback there -- see the README's Spotify section and
+17 -17
View File
@@ -107,7 +107,7 @@ class Controller:
"""The AVR is always a plain player. Resolving it by player name
matters: once it leads a group, HEOS also reports a *group* under
the very same name."""
target = self.cfg.TARGETS[self.cfg.HOST_KEY]
target = self.cfg.ROOMS[self.cfg.HOST_ROOM]
pid = self._player_pid(target["heos_name"])
if pid is None:
raise TargetError(f"No HEOS player named '{target['heos_name']}' (the AVR) was found")
@@ -115,12 +115,12 @@ class Controller:
def member_pids(self, key: str) -> list:
"""The pid of the one player that is this room, as a list."""
if key not in self.cfg.TARGETS:
if key not in self.cfg.ROOMS:
raise TargetError(f"Unknown room '{key}'")
if key == self.cfg.HOST_KEY:
if key == self.cfg.HOST_ROOM:
return [self._host_pid()]
name = self.cfg.TARGETS[key]["heos_name"]
name = self.cfg.ROOMS[key]["heos_name"]
pid = self._player_pid(name)
if pid is None:
raise TargetError(
@@ -221,7 +221,7 @@ class Controller:
def joined_keys(self) -> list:
joined, host_pids = [], self._host_group_pids()
for key in self.cfg.ROOM_KEYS:
for key in self.cfg.GUEST_ROOMS:
try:
if host_pids & set(self.member_pids(key)):
joined.append(key)
@@ -256,7 +256,7 @@ class Controller:
with self._lock:
self.scan()
wanted = set(self.joined_keys()) | {key}
self.group_targets(self.cfg.HOST_KEY, [k for k in self.cfg.ROOM_KEYS if k in wanted])
self.group_targets(self.cfg.HOST_ROOM, [k for k in self.cfg.GUEST_ROOMS if k in wanted])
self.scan()
self._nudge_avr_input()
return self.joined_keys()
@@ -274,7 +274,7 @@ class Controller:
try:
current = self.avr_current_input()
if current:
self.play_heos_input(self.cfg.HOST_KEY, current["code"])
self.play_heos_input(self.cfg.HOST_ROOM, current["code"])
except HeosError:
pass
@@ -292,12 +292,12 @@ class Controller:
"""Make the AVR's group contain exactly these rooms and no others."""
with self._lock:
self.scan()
wanted = [k for k in self.cfg.ROOM_KEYS if k in joined]
wanted = [k for k in self.cfg.GUEST_ROOMS if k in joined]
for key in list(self.joined_keys()):
if key not in wanted:
self.ungroup(key)
if wanted:
self.group_targets(self.cfg.HOST_KEY, wanted)
self.group_targets(self.cfg.HOST_ROOM, wanted)
self.scan()
return self.joined_keys()
@@ -414,7 +414,7 @@ class Controller:
"""{"code", "name"} pairs for the picker -- heos_inputs()'s shape,
renamed to match what the UI and /api/avr/* already send and
expect."""
sources = self.heos_inputs(self.cfg.HOST_KEY)
sources = self.heos_inputs(self.cfg.HOST_ROOM)
return [{"code": s["input_id"], "name": s["name"]} for s in sources]
def avr_current_input(self):
@@ -435,7 +435,7 @@ class Controller:
return {"code": mid, "name": payload.get("station") or payload.get("song") or mid}
def avr_select_input(self, code: str) -> dict:
self.play_heos_input(self.cfg.HOST_KEY, code)
self.play_heos_input(self.cfg.HOST_ROOM, code)
name = next((s["name"] for s in self.avr_inputs() if s["code"] == code), code)
return {"code": code, "name": name}
@@ -476,7 +476,7 @@ class Controller:
matched up as replies come back rather than asked for one at a
time."""
snapshot = {
"host": {"key": self.cfg.HOST_KEY, "label": self.cfg.TARGETS[self.cfg.HOST_KEY]["label"]},
"host": {"key": self.cfg.HOST_ROOM, "label": self.cfg.ROOMS[self.cfg.HOST_ROOM]["label"]},
"rooms": [],
"avr": {"connected": False, "input": None, "inputs": [], "volume": None},
"heos_ok": True,
@@ -491,7 +491,7 @@ class Controller:
joined = set(self.joined_keys())
room_pids, room_errors = {}, {}
for key in self.cfg.ROOM_KEYS:
for key in self.cfg.GUEST_ROOMS:
try:
room_pids[key] = self.member_pids(key)[0]
except TargetError as exc:
@@ -526,10 +526,10 @@ class Controller:
else:
by_room.setdefault(key, {})[field] = result
for key in self.cfg.ROOM_KEYS:
for key in self.cfg.GUEST_ROOMS:
room = {
"key": key,
"label": self.cfg.TARGETS[key]["label"],
"label": self.cfg.ROOMS[key]["label"],
"available": True,
"grouped": key in joined,
"volume": None,
@@ -574,10 +574,10 @@ class Controller:
snapshot["heos_ok"] = False
snapshot["errors"].append(str(exc))
snapshot["rooms"] = [
{"key": key, "label": self.cfg.TARGETS[key]["label"], "available": False,
{"key": key, "label": self.cfg.ROOMS[key]["label"], "available": False,
"grouped": False, "volume": None, "play_state": None, "spotify": False,
"now_playing": None, "error": str(exc)}
for key in self.cfg.ROOM_KEYS
for key in self.cfg.GUEST_ROOMS
]
try:
+10 -10
View File
@@ -52,11 +52,11 @@ class DemoController:
self.cfg = cfg
self.avr = _FakeAvr()
self.heos = None
self._volume = {key: 22 + 7 * i for i, key in enumerate(cfg.TARGETS)}
self._play = {key: "play" for key in cfg.TARGETS}
self._volume = {key: 22 + 7 * i for i, key in enumerate(cfg.ROOMS)}
self._play = {key: "play" for key in cfg.ROOMS}
self._joined = set()
# One room on Spotify and one not, so both kinds of card show up.
self._spotify = {key: i == 0 for i, key in enumerate(cfg.ROOM_KEYS)}
self._spotify = {key: i == 0 for i, key in enumerate(cfg.GUEST_ROOMS)}
# No cover, since the demo has no network to fetch one from.
self._track = {"song": "Harvest Moon", "artist": "Neil Young", "image": None}
# Its play button only shows for a stream one of your accounts plays.
@@ -67,19 +67,19 @@ class DemoController:
# -- what the UI uses ---------------------------------------------
def state(self):
return {
"host": {"key": self.cfg.HOST_KEY, "label": self.cfg.TARGETS[self.cfg.HOST_KEY]["label"]},
"host": {"key": self.cfg.HOST_ROOM, "label": self.cfg.ROOMS[self.cfg.HOST_ROOM]["label"]},
"rooms": [
{"key": key, "label": self.cfg.TARGETS[key]["label"], "available": True,
{"key": key, "label": self.cfg.ROOMS[key]["label"], "available": True,
"grouped": key in self._joined, "volume": self._volume[key],
"play_state": self._play[key], "spotify": self._spotify[key],
"spotify_account": self._account if self._spotify[key] else None,
"now_playing": self._track if self._spotify[key] and self._play[key] != "stop" else None,
"error": None}
for key in self.cfg.ROOM_KEYS
for key in self.cfg.GUEST_ROOMS
],
"avr": {
"connected": True, "inputs": self.avr.inputs(), "input": self.avr.current_input(),
"volume": self._volume[self.cfg.HOST_KEY],
"volume": self._volume[self.cfg.HOST_ROOM],
# Standing in for a Zidoo plugged into this input, so the
# AVR card's now-playing block has something to show here too.
"now_playing": {"song": "Big Buck Bunny", "artist": "2008", "image": None,
@@ -126,7 +126,7 @@ class DemoController:
return self.joined_keys()
def set_membership(self, joined):
self._joined = {k for k in self.cfg.ROOM_KEYS if k in joined}
self._joined = {k for k in self.cfg.GUEST_ROOMS if k in joined}
return self.joined_keys()
def zidoo_play_state(self, state=None):
@@ -141,13 +141,13 @@ class DemoController:
self._film_ms = min(596000, int(position_ms))
def joined_keys(self):
return [k for k in self.cfg.ROOM_KEYS if k in self._joined]
return [k for k in self.cfg.GUEST_ROOMS 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())],
for i, t in enumerate(self.cfg.ROOMS.values())],
"groups": [],
}
+11 -35
View File
@@ -5,6 +5,9 @@
const STEP = Number(document.documentElement.dataset.step) || 2;
// Where the app is mounted: "/" on its own port, "/heos/" behind a proxy.
const BASE = document.documentElement.dataset.base || '/';
// The commit this page was rendered from, so a resumed iOS home-screen
// session can tell the Pi has moved on and reload itself.
const VERSION = document.documentElement.dataset.version || '';
const POLL_MS = 5000;
// How long a dropped cursor waits for the player to report it got there.
const SEEK_HOLD_MS = 10000;
@@ -13,8 +16,6 @@ const SEEK_KEY_MS = 10000;
// And how long a pressed play or pause on the AVR waits for the same. The
// Zidoo acknowledges the key press at once but takes a moment to act on it.
const PLAY_HOLD_MS = 6000;
// Motion turned down in the OS means cards change places without sliding.
const CALM = matchMedia('(prefers-reduced-motion: reduce)');
// Which way each arrow key moves a slider.
const ARROWS = { ArrowRight: 1, ArrowUp: 1, ArrowLeft: -1, ArrowDown: -1 };
@@ -77,10 +78,6 @@ const ui = {
// theirs do. Which one it is, everything that only it can do goes through.
const HOST = ui.avr.dataset.room;
// Named, so a view transition can follow the card when it moves to the top
// and back. A room's name is set as it is placed; the host's never moves.
ui.avr.style.viewTransitionName = 'avr';
const rooms = {};
let inputs = [];
let currentInput = null;
@@ -229,7 +226,6 @@ function paintRoom(room) {
paintTransport(room);
paintSources(room);
placeRoom(room);
placeCards();
}
/* Song, artist, cover and progress, each only when there is one. The
@@ -432,10 +428,6 @@ function placeRoom(room) {
// Nothing left in the column below it means it goes last, before the host.
ui.app.insertBefore(room.node, next || ui.avr);
}
// A view transition only follows an element it can name, and the name has
// to be one of a kind on the page (see placeCards). Inside the host's card
// the card travels with it, so it takes no name of its own there.
room.node.style.viewTransitionName = room.grouped ? 'none' : `room-${room.key}`;
}
function paintGrouping() {
@@ -453,30 +445,6 @@ function paintGrouping() {
const playing = inside.some((node) => node.classList.contains('playing'))
|| shownPlayState(rooms[HOST]) === 'play';
ui.avr.classList.toggle('playing', playing);
placeCards();
}
let placing = false;
function placeCards() {
if (placing) return;
placing = true;
queueMicrotask(() => {
placing = false;
// Grouped, a room's card has left the column and the AVR's is the one
// that moves up. The AVR's tint and its position answer the same
// question, and paintGrouping() has just settled it.
const wanted = Object.values(rooms).map((room) => [
room.node,
// The AVR's tint already answers this: paintGrouping has just settled
// it, over the whole group rather than the host's own stream alone.
room.avr ? room.node.classList.contains('playing') : room.playing() && !room.grouped,
]);
const move = () => wanted.forEach(([node, top]) => node.classList.toggle('on-top', top));
const moves = wanted.some(([node, top]) => node.classList.contains('on-top') !== top);
if (!moves || CALM.matches || !document.startViewTransition) move();
else document.startViewTransition(move);
});
}
/* Press and hold to keep moving, accelerating as you hold. */
@@ -830,6 +798,14 @@ function applyRoom(room, incoming) {
}
function render(state) {
// The Pi shipped a newer version while this tab (or a resumed iOS
// home-screen session) was still showing an old one -- catch up rather
// than keep polling a page that no longer matches the server.
if (VERSION && state.version && state.version !== VERSION && !busy()) {
location.reload();
return;
}
(state.rooms || []).forEach((incoming) => applyRoom(rooms[incoming.key], incoming));
const avr = state.avr || {};
+3 -12
View File
@@ -47,13 +47,13 @@
@media (prefers-color-scheme: dark) {
:root {
--bg: #0a0d14;
--card: #151a25;
--card: #202736;
--edge: #232b3b;
--ink: #eef2f9;
--muted: #98a3b8;
--outline: #5f6b87;
--playing: #1f4a42;
--raised: #29334a;
--raised: #34405b;
--scrim: rgba(4, 6, 11, .6);
--shadow: rgba(0, 0, 0, .45);
--shade: rgba(255, 255, 255, .05);
@@ -135,7 +135,7 @@ svg { width: 22px; height: 22px; fill: currentColor; }
.ghost { background: transparent; border: 1px solid var(--outline); color: var(--muted); box-shadow: none; } /* nothing to lift */
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes rise { from { transform: translateY(14%); opacity: .4; } }
@keyframes rise { from { transform: translateY(100%); opacity: .4; } }
/* Header */
@@ -148,15 +148,6 @@ header .logo { height: 44px; width: auto; display: block; margin: var(--space-xs
/* Card */
/* A playing card goes to the top. order does not transition, so the swap
rides on a view transition instead -- see placeCards() in app.js. */
.app > .on-top { order: -1; }
::view-transition-group(*) {
animation-duration: .4s;
animation-timing-function: cubic-bezier(.22, 1, .36, 1);
}
.card {
background: var(--card);
border-radius: var(--radius-xl);
+2 -2
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" data-step="{{ step }}" data-host="{{ host.label }}" data-base="{{ url_for('index') }}">
<html lang="en" data-step="{{ step }}" data-host="{{ host.label }}" data-base="{{ url_for('index') }}" data-version="{{ version }}">
<head>
<meta charset="utf-8">
@@ -130,7 +130,7 @@
<img class="logo" src="{{ url_for('static', filename='logo.svg') }}" alt="{{ app_name }}">
</header>
{% for room in rooms + [host] %}
{% for room in rooms %}
{{ room_card(room, spotify_accounts) }}
{% endfor %}
+3 -3
View File
@@ -22,9 +22,9 @@ def build():
heos = FakeHeos()
cfg = SimpleNamespace(
HEOS_HOST="127.0.0.1", HEOS_PORT=heos.port,
HOST_KEY="avr",
ROOM_KEYS=["lego_room", "living_room"],
TARGETS={
HOST_ROOM="avr",
GUEST_ROOMS=["lego_room", "living_room"],
ROOMS={
"avr": {"label": "Home Cinema", "heos_name": "Home Cinema"},
"lego_room": {"label": "Lego Room", "heos_name": "Lego Room"},
"living_room": {"label": "Living Room", "heos_name": "Denon Home 200 L"},
+2 -2
View File
@@ -169,8 +169,8 @@ class ZidooThroughTheControllerTest(unittest.TestCase):
threading.Thread(target=self.server.serve_forever, args=(0.05,), daemon=True).start()
self.cfg = SimpleNamespace(
HEOS_HOST="127.0.0.1", HEOS_PORT=1255,
HOST_KEY="avr", ROOM_KEYS=[],
TARGETS={"avr": {"label": "Home Cinema", "heos_name": "Home Cinema"}},
HOST_ROOM="avr", GUEST_ROOMS=[],
ROOMS={"avr": {"label": "Home Cinema", "heos_name": "Home Cinema"}},
VOLUME_STEP=5,
ZIDOO_HOST="127.0.0.1", ZIDOO_PORT=self.server.server_address[1],
)