"""Everything you might want to change lives here. Copy the values out of `GET /api/targets` (or the old `/targets`) the first time you run this, so the names below match exactly what HEOS reports for your own devices. """ import json import os from pathlib import Path def _load_dotenv(path: Path): """A `source .env` a shell might forget to `export` is a whole class of bug this sidesteps: read the file directly, rather than trusting whatever the calling shell's environment happens to contain. Existing environment variables still win, so a real export (or systemd's EnvironmentFile) overrides the file rather than the other way round.""" if not path.exists(): return for line in path.read_text().splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: continue key, _, value = line.partition("=") os.environ.setdefault(key.strip(), value.strip()) _load_dotenv(Path(__file__).resolve().parent / ".env") # --- Rooms, ports & behaviour ------------------------------------------- # None of this is sensitive -- it doesn't give away anything about your # LAN or credentials -- but it's still yours, not the app's: buying a # speaker, renaming a room, or changing a port is a config edit, not a # code change. Lives in config.json rather than here or in .env; copy # config.json.example to get started. # # heos_name : the EXACT name HEOS reports for that player -- including # an In-Room Group like a stereo pair, which HEOS pairs at # the hardware level into one player, one pid, always. # spotify_name : only needed if a room's Spotify Connect name differs # from heos_name -- GET /api/spotify/devices?account=account1 shows what # Spotify actually calls it. Defaults to heos_name. # in_room_group : what heos_name actually names, when it is not a single # speaker -- one of IN_ROOM_GROUPS below. HEOS merges any # of these into one player, one pid, at the hardware # level, the same way it does a Stereo Pair -- so, same # as heos_name itself, this can only be set by hand, not # read back from HEOS. Defaults to "none". IN_ROOM_GROUPS = ("none", "stereo-pair", "lcr-fronts", "surround-sound-system", "subwoofer") _config_path = Path(__file__).resolve().parent / "config.json" try: _cfg = json.loads(_config_path.read_text()) except FileNotFoundError: raise SystemExit( f"{_config_path} not found -- copy config.json.example to config.json " "and fill in your own rooms (see the README's Configure section)." ) TARGETS = _cfg["targets"] for _key, _target in TARGETS.items(): _group = _target.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)}" ) # 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"] # The rooms that get a card with volume + a join/leave button, in order. ROOM_KEYS = _cfg["room_keys"] # 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"] # --- Network ---------------------------------------------------------- # Any ONE HEOS device's IP is enough: HEOS is a distributed system, so # whichever unit you connect to can see and control every player and # group on the network. A LAN address isn't a credential, so it lives # here rather than in .env -- just don't publish this file if your LAN # is reachable from outside it. HEOS_HOST = _cfg["heos_host"] HEOS_PORT = _cfg["heos_port"] # --- Zidoo (optional) --------------------------------------------------- # A Zidoo media player plugged into one of the AVR's inputs. HEOS only # knows that input is selected, not what the Zidoo is actually showing, so # its "now playing" comes from the Zidoo's own HTTP API instead -- queried # only while ZIDOO_INPUT_CODE is the AVR's selected input. Leave # zidoo_host out of config.json if there is no Zidoo to ask. ZIDOO_HOST = _cfg.get("zidoo_host") ZIDOO_PORT = _cfg["zidoo_port"] ZIDOO_INPUT_CODE = "inputs/mediaplayer" # see GET /api/avr/inputs # Shown as the app's name on the iOS home screen. APP_NAME = "Heos" # --- 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 # tools/spotify_auth.py. Credentials come from .env, never from here, so # they stay out of git. One developer app serves every account; only the # refresh token differs, since that is what each account's login produces. SPOTIFY_CLIENT_ID = os.environ.get("SPOTIFY_CLIENT_ID") SPOTIFY_CLIENT_SECRET = os.environ.get("SPOTIFY_CLIENT_SECRET") # key -> button label and that account's refresh token, in button order. # An account without a token gets no button. The label comes from # spotify_accounts in config.json -- e.g. "account1": "Fifou". _spotify_names = _cfg.get("spotify_accounts", {}) SPOTIFY_ACCOUNTS = { "account1": {"label": _spotify_names.get("account1", "Account 1"), "refresh_token": os.environ.get("SPOTIFY_ACCOUNT1_REFRESH_TOKEN")}, "account2": {"label": _spotify_names.get("account2", "Account 2"), "refresh_token": os.environ.get("SPOTIFY_ACCOUNT2_REFRESH_TOKEN")}, }