Перейти к содержимому

7–10 марта: Надёжность, multi-bridge и HA OAuth

7 марта 2026 — Надёжность и деплой (v2.10.7 → v2.12.0)

Заголовок раздела «7 марта 2026 — Надёжность и деплой (v2.10.7 → v2.12.0)»

Перестройка архитектуры громкости (v2.10.7 – v2.10.13)

Заголовок раздела «Перестройка архитектуры громкости (v2.10.7 – v2.10.13)»

Гибридный путь громкости — маршрутизация команд через MA WebSocket API для синхронизации UI MA — создал тройной feedback loop: API, эхо протокола sendspin и событие MA-монитора одновременно пытались установить громкость PulseAudio sink. Результат — скачки громкости (установлено 40 → прыгает на 47 → устаканивается на 55) и неожиданные изменения при смене трека.

Исправление было архитектурным: bridge_daemon стал единственным писателем громкости PulseAudio sink. API больше не обновляет оптимистично локальный статус при MA-пути — он ждёт реального эха от MA через протокол sendspin. Синхронизация громкости _handle_player_updated в MA-мониторе была удалена как избыточный третий путь. Новая опция конфигурации VOLUME_VIA_MA (по умолчанию: true) позволяет полностью отключить MA-прокси, направляя все изменения громкости/mute через прямой pactl.

Наблюдаемость и тестовая инфраструктура (v2.10.8)

Заголовок раздела «Наблюдаемость и тестовая инфраструктура (v2.10.8)»

Все 27 молчаливых блоков except: pass были заменены на DEBUG-уровневое логирование — проблемы теперь видны с LOG_LEVEL=DEBUG без изменения runtime-поведения. Потокобезопасность была укреплена: вызовы run_coroutine_threadsafe получили 5-секундные таймауты, а fire-and-forget asyncio-задачи получили done_callback для логирования исключений. Проект обзавёлся первыми автоматическими тестами: pytest с 9 юнит-тестами, покрывающими загрузку конфига, сохранение громкости, маппинг MAC→player-ID и хеширование паролей (позже расширено до 15 тестов).

LXC-инсталлятор был обновлён для скачивания всех модулей приложения (config, state, routes, services, templates, static) вместо изначальных 2 файлов. Конфигурация PulseAudio была исправлена для PA 17+ на Ubuntu 24.04: устаревший enable-lfe-remixing заменён на remixing-produce-lfe/remixing-consume-lfe, systemd-юнит больше не устанавливает User=pulse/Group=pulse (PA в режиме --system требует root), а tmpfiles.d-запись гарантирует, что /var/run/pulse переживёт перезагрузки.

Новый инсталлятор lxc/install-openwrt.sh добавил поддержку роутеров на OpenWrt (Turris Omnia и т.д.) с управлением сервисом через procd — расширив варианты деплоя с 3 (Docker, HA addon, Proxmox LXC) до 4.

Watchdog зомби-воспроизведения и изоляция churn (v2.12.0)

Заголовок раздела «Watchdog зомби-воспроизведения и изоляция churn (v2.12.0)»

Были добавлены две функции надёжности:

  • Watchdog зомби-воспроизведения: автоматически перезапускает подпроцесс после 15 секунд состояния playing=True без аудиоданных (streaming=False), до 3 попыток. Ловит ситуации, когда sendspin-соединение живо, но аудиопайплайн сломан.
  • Изоляция BT churn (opt-in): автоматически отключает BT-управление для устройств, которые переподключаются слишком часто в скользящем окне, настраивается через BT_CHURN_THRESHOLD (0 = отключено, по умолчанию) и BT_CHURN_WINDOW (по умолчанию 300 с). Предотвращает ситуацию, когда нестабильное Bluetooth-устройство занимает время адаптера и дестабилизирует другие колонки.

Новый индикатор застывшего эквалайзера показывает замороженные красные полосы, когда MA сообщает о воспроизведении, но аудио не стримится, с текстом «▶ No Audio».

8 марта 2026 — Multi-bridge и сообщество (v2.12.1 → v2.13.1)

Заголовок раздела «8 марта 2026 — Multi-bridge и сообщество (v2.12.1 → v2.13.1)»

Кэширование, надёжность SSE и фиксы HA Ingress (v2.12.1 → v2.12.6)

Заголовок раздела «Кэширование, надёжность SSE и фиксы HA Ingress (v2.12.1 → v2.12.6)»

Серия быстрых релизов решала поступательно обнаруженные проблемы поведения HA Ingress proxy: cache-busting статических ресурсов через query string (?v=) не работал, потому что Ingress обрезает query-параметры — переключились на path-based версионирование (/static/v2.12.5/app.js). HTML-ответы получили заголовки Cache-Control: no-cache. SSE-поток получил 2 КБ начального padding для сброса proxy-буферов, а клиентская логика переподключения SSE была обновлена с «один сбой → polling навсегда» на экспоненциальный backoff с 5 попытками.

Sendspin daemon теперь запускается только после реального подключения Bluetooth, устраняя фантомные плееры в Music Assistant при старте контейнера.

Анализ и улучшения архитектуры multi-bridge (v2.13.0)

Заголовок раздела «Анализ и улучшения архитектуры multi-bridge (v2.13.0)»

Глубокий анализ сценария multi-bridge (несколько мостов → один MA, cross-bridge sync-группы) выявил 6 потенциальных проблем и привёл к двум ключевым улучшениям:

  • Автозаполнение BRIDGE_NAME: при первом запуске hostname машины записывается в config.json["BRIDGE_NAME"], чтобы пользователи видели предзаполненное значение в веб-интерфейсе до добавления устройств. Старый BRIDGE_NAME_SUFFIX boolean был удалён — больше не нужен, когда имя заполняется автоматически. Это предотвращает дублирование имён плееров (например, два «JBL Flip 6» с разных хостов), которое путало список плееров MA.

  • Видимость cross-bridge sync-групп: когда плееры из нескольких мостов входят в одну MA sync-группу, бейдж группы теперь показывает 🔗 Kitchen Music +2 (где +2 = плееры с других мостов). При наведении на бейдж раскрывается полный список участников с ✓ для локальных и 🌐 для внешних плееров. Данные берутся из кэша MA API (/api/players → списки участников sync-групп), который мост уже поддерживает.

Деплой v2.13.0 на два живых LXC-моста (Proxmox + Turris OpenWrt) выявил цепочку проблем:

  • Waitress 3.x сломал SSE: обновление waitress подтянуло v3.x, который строго соблюдает PEP 3333 и отклоняет hop-by-hop заголовки. Connection: keep-alive в SSE-ответе вызывал краш AssertionError — заголовок полностью удалён.
  • Несовпадение имени JS-переменной: и polling-, и SSE-обработчики в app.js обращались к data.groups, но распарсенная переменная называлась status — устройства никогда не рендерились. Исправлено на status.groups.
  • Несовпадение ID при обогащении групп: _build_groups_summary() сравнивал group_id Sendspin (UUID) с syncgroup ID MA (syncgroup_XXX) — разные системы ID, которые никогда не совпадали. Исправлено через резолвинг MA syncgroup по маппингу имён плееров.
  • Группы отсутствовали в ответе polling: /api/status для мостов с одним устройством не включал поле groups (оно было только в SSE), поэтому бейдж никогда не появлялся через polling.
  • Инцидент с bluetooth.service в LXC: случайный перезапуск bluetooth.service внутри контейнера Turris (где bluetoothd не может работать) сломал A2DP-состояние PulseAudio, потребовав повторное сопряжение устройств с хоста. Защита: bluetooth.service теперь замаскирован (не просто disabled), а sendspin-client.service получил TimeoutStopSec=15 для предотвращения зависших остановок.

Проект получил структурированное управление задачами: 3 YAML-шаблона issue (Bug Report с dropdown-ами deployment/audio, специализированная форма Bluetooth/Audio, Feature Request), 16 меток проекта (type:bug, area:bluetooth, deploy:ha-addon и т.д.) и приветственный пост в Discussions с руководством по маршрутизации (Issues для багов/фич, Discussions для помощи/идей).

Комплексное укрепление безопасности и аудит качества кода (v2.16.0)

Заголовок раздела «Комплексное укрепление безопасности и аудит качества кода (v2.16.0)»

Полный code review всей кодовой базы выявил 42 проблемы в области безопасности, потокобезопасности, обработки ошибок, надёжности и покрытия тестами. Все были решены в одном координированном релизе:

Безопасность (5 фиксов): SSRF через path traversal flow_id в потоке HA auth; SSE endpoint мог исчерпать все Waitress-потоки (ограничено до 4); нелимитированная громкость с сервера могла перегрузить колонки на 200%+; инъекция MAC-адреса в stdin bluetoothctl; /api/status раскрывал MAC, IP и метаданные плееров без аутентификации.

Потокобезопасность (6 фиксов): итерация по списку _clients без блокировки в ~15 API endpoint-ах; stop_sendspin() обходил SSE-уведомление; race condition счётчика перезапусков зомби; чтение конфиг-файла без config_lock; несинхронизированная запись учётных данных MA API; пул BT executor слишком мал (2→4) для переподключения нескольких устройств.

Обработка ошибок и валидация входных данных (7 фиксов): краш request.get_json() на не-JSON POST; утечка внутренних строк исключений в 15 ответах об ошибках; краш IPC-команды громкости на нечисловом вводе; path traversal через подставной client_id; путаница типов player_names (string vs list); set_log_level принимал произвольные цели getattr; force=True ослаблял CSRF-защиту на endpoint пароля.

Покрытие тестами (65 новых тестов): с 42 до 107 тестов. Новые тестовые файлы для services/bluetooth.py, services/pulse.py, bluetooth_manager.py, services/daemon_process.py, scripts/translate_ha_config.py и routes/api.py. Добавлен общий conftest.py. datetime.UTC заменён на timezone.utc в 4 файлах для совместимости тестов с Python 3.9.

Совместимость с armv7l (hotfix после релиза): PyAV 12.3.0 (единственная версия, компилируемая на armv7l) не имеет AudioLayout.nb_channels, из-за чего FLAC-декодер sendspin падает с AttributeError — полная тишина. Monkey-patch в services/daemon_process.py заменяет FlacDecoder._append_frame_to_pcm на версию, использующую len(frame.layout.channels). Патч автоматически определяет версию PyAV при запуске и является no-op на PyAV 13+.

Raspberry Pi и Docker UX (v2.16.2): После того как первый пользователь из сообщества попробовал Docker на Raspberry Pi и столкнулся с проблемами конфигурации, мы добавили: диагностический скрипт pre-flight (scripts/rpi-check.sh), проверяющий Docker, Bluetooth, аудио, UID и архитектуру перед docker compose up; endpoint /api/preflight без аутентификации для программной проверки настройки; структурированную таблицу диагностики запуска в entrypoint.sh (видна в docker logs); специальное руководство по установке на Raspberry Pi (en/ru); и исправленную устаревшую Docker-документацию, которая всё ещё упоминала удалённую capability SYS_ADMIN и не содержала переменных окружения PULSE_SERVER/XDG_RUNTIME_DIR.


10 марта 2026 — HA OAuth и аутентификация MA API (v2.17.0–v2.20.0, ~45 коммитов)

Заголовок раздела «10 марта 2026 — HA OAuth и аутентификация MA API (v2.17.0–v2.20.0, ~45 коммитов)»

В режиме addon MA находится в приватной Docker-сети — недоступен из браузера пользователя. Мост добавил поток HA OAuth popup: веб-интерфейс открывает popup на endpoint авторизации HA OAuth, HA аутентифицирует пользователя (включая 2FA/TOTP), а мост обменивает полученный код на сессионный токен MA через серверные HTTP-вызовы через HA Ingress. Это устраняет необходимость ручной настройки MA_API_TOKEN.

Поток popup требовал взаимодействия с пользователем. В режиме Ingress сессионный токен HA уже доступен в localStorage (hassTokens). Мост теперь считывает его автоматически при загрузке страницы, вызывает /api/ma/ha-silent-auth, который выполняет полный OAuth-обмен на стороне сервера — ноль кликов. Auto-discover тоже запускается при загрузке страницы, так что соединение с MA устанавливается прозрачно.

Расследование persistent-ошибок «authentication failed» в MA-мониторе выявило фундаментальную проблему: OAuth callback возвращает сессионный JWT с коротким сроком жизни (30-дневный скользящий срок, is_long_lived=False), а не API-токен. Кроме того, баг в регулярном выражении захватывал #/ (Vue Router hash fragment) как часть JWT, повреждая его.

Исправление: после получения сессионного JWT через OAuth мост подключается к WebSocket API MA, аутентифицируется сессионным токеном и вызывает auth/token/create для получения полноценного долгоживущего JWT (10-летний срок). Сессионный токен никогда не сохраняется.

Идемпотентность: перед началом OAuth _validate_ma_token() проверяет, действителен ли существующий токен для целевого MA URL — предотвращая создание дублирующих долгоживущих токенов при перезагрузке страницы или перезапуске addon-а.

Обнаружение MA-сервера из sendspin-соединения (v2.17.9)

Заголовок раздела «Обнаружение MA-сервера из sendspin-соединения (v2.17.9)»

В режиме addon с SENDSPIN_SERVER=auto обнаружение MA-сервера в крайнем случае опиралось на mDNS — но изменение API zeroconf (kwargs вместо позиционных аргументов) сломало коллбэк. Исправление: перед откатом к mDNS мост теперь извлекает хост MA-сервера из резолвенного WebSocket-соединения sendspin (connected_server_url). Поскольку sendspin уже обнаружил MA-сервер через собственный mDNS, мост переиспользует этот резолвенный адрес для endpoint MA API (тот же хост, порт 8095). Это устраняет необходимость отдельного mDNS-сканирования в большинстве случаев.

Упрощённый addon discovery и полуавтоматическая аутентификация (v2.17.10)

Заголовок раздела «Упрощённый addon discovery и полуавтоматическая аутентификация (v2.17.10)»

Предыдущий подход имел фундаментальную проблему: определение addon-режима зависело от поля homeassistant_addon MA-сервера из его endpoint /info — но когда discovery использовал mDNS-путь (через _enrich_with_server_info вместо validate_ma_url), это поле отсутствовало, поэтому addon-режим никогда не определялся и тихая аутентификация никогда не срабатывала.

Исправление упростило весь поток. Мост теперь сообщает собственный флаг is_addon (из _detect_runtime()) в ответе discover — без зависимости от метаданных MA-сервера. В addon-режиме discovery сначала пробует http://homeassistant.local:8095 (внутренний DNS Supervisor — практически мгновенно), минуя эвристики SENDSPIN_SERVER и mDNS. Полностью автоматическая тихая аутентификация при загрузке страницы была заменена полуавтоматическим подходом: кнопка «Sign in with Home Assistant» показывается после того, как discover обнаружит addon-режим, и пользователь кликает на неё явно. В Ingress-режиме это выполняет аутентификацию в один клик (без popup); вне Ingress — открывает OAuth popup.

Беспарольная MA-аутентификация через Ingress JSONRPC (v2.18.0)

Заголовок раздела «Беспарольная MA-аутентификация через Ingress JSONRPC (v2.18.0)»

Тихая аутентификация в v2.17.4–v2.17.12 пыталась выполнить POST на /auth/authorize HA с Bearer-токеном для получения OAuth-кода — но endpoint авторизации HA работает только на GET (он отдаёт HTML-страницу согласия) и возвращает HTTP 405. Fallback через popup работал, но требовал ввода учётных данных.

Подход v2.18.0 полностью обходит HA OAuth. Ingress-сервер MA (порт 8094) автоматически аутентифицирует запросы через заголовки X-Remote-User-ID / X-Remote-User-Name — тот же механизм, который HA использует внутренне для Ingress-трафика. Поскольку оба addon-а используют host_network: true, мост может достучаться до Ingress-порта MA на localhost:8094. Поток: (1) фронтенд отправляет HA access token из hassTokens в localStorage; (2) бэкенд подключается к WebSocket API HA и вызывает auth/current_user для получения ID и username пользователя; (3) бэкенд отправляет JSONRPC-запрос на Ingress endpoint MA (http://localhost:8094/api) с заголовками пользователя, вызывая auth/token/create; (4) MA автоматически аутентифицирует Ingress-запрос и создаёт долгоживущий 10-летний JWT. Весь поток невидим для пользователя — один клик кнопки, без учётных данных, без popup.

Три быстрых патча решили реальные проблемы деплоя, обнаруженные при верификации на HAOS:

v2.18.1 — совместимость websockets. Docker-образ HAOS addon поставляется со старой библиотекой websockets (<14), которая не принимает именованный аргумент proxy=None. Был добавлен compatibility-враппер _ws_connect(), который сначала пробует с proxy=None, перехватывает TypeError и повторяет без него.

v2.18.2 — сеть HAOS addon. В HAOS каждый addon работает в собственном Docker-контейнере со своим сетевым namespace — localhost:8094 из addon моста не достигает Ingress-порта MA. Исправление: _find_ma_ingress_url() запрашивает Supervisor API HA (http://supervisor/addons/{slug}/info) для обнаружения Docker-hostname и Ingress-порта addon MA, затем подключается через Docker DNS (например, http://d5369777-music-assistant:8094). Известные slug-и addon MA (d5369777_music_assistant, _beta, _dev) перебираются по порядку. Конфигурация addon получила разрешения hassio_api: true и homeassistant_api: true.

v2.18.3 — формат ответа JSONRPC. auth/token/create MA возвращает токен как сырую JSON-строку при вызове через Ingress-порт, а не обёрнутым в {"result": "..."}. Парсер ответа теперь обрабатывает оба формата и логирует сырой ответ для диагностики.

Раздел Configuration разросся органически и требовал реструктуризации. Кнопки сохранения были посередине формы, Music Assistant Integration была спрятана внутри Advanced settings (два клика вглубь), таблица BT Devices имела 9 столбцов с горизонтальным скроллом 700px на мобильных, а подписи были многословными абзацами.

Перестройка реорганизовала форму в чётко обозначенные секции — General, Bluetooth, Music Assistant (вынесен на верхний уровень), Advanced и Authentication — каждая с иконкой в заголовке и визуальным разделением. Sticky save bar теперь появляется внизу, когда в конфиге есть несохранённые изменения. Таблица BT Devices была разделена на основную строку (Name, MAC, Adapter, Format) и раскрывающуюся дополнительную строку для расширенных полей (Listen Address, Port, Delay, Keep-alive), которая автоматически открывается при нестандартных значениях.

Полировка UX конфигурации и обратная связь от сообщества (v2.20.0)

Заголовок раздела «Полировка UX конфигурации и обратная связь от сообщества (v2.20.0)»

Обратная связь сообщества по релизу v2.19.0 привела ко второму раунду полировки. Пользователи отметили, что кнопка Add в списке отсканированных/сопряжённых устройств была слишком далеко от имени устройства, что затрудняло нацеливание. Панель Advanced settings (в которой к этому моменту осталось всего 4 поля) была полностью ликвидирована — поля перенесены в соответствующие секции, лишняя панель удалена.

Ключевые изменения: MA-форма теперь автоматически сворачивается в сводку при подключении (ссылка «Reconfigure» раскрывает её); поля auth скрываются, когда отключены; шеврон раскрытия BT-устройства перенесён на левую сторону строки для привычного tree-style взаимодействия; устройства по умолчанию свёрнуты; строки scan/paired устройств стали полностью кликабельными с hover-подсветкой; кнопка Scan перенесена перед +Add Device для discovery-first workflow. Добавлен guard _configLoading для предотвращения срабатывания индикатора несохранённых изменений при программном заполнении полей при загрузке страницы.

Аудит кода и внутренний рефакторинг (v2.20.3)

Заголовок раздела «Аудит кода и внутренний рефакторинг (v2.20.3)»

Комплексный code review всей кодовой базы (~10 700 строк в 35 Python-файлах) выявил две критические проблемы: мёртвый endpoint /api/bt/reconnect (функция существовала, но не имела декоратора @route — ни один HTTP-запрос не мог до неё добраться) и wildcard postMessage('*') в коллбэке OAuth popup HA, нарушавший принцип same-origin. Обе были исправлены немедленно.

Более масштабным результатом стало разделение монолита routes/api.py в 3 178 строк — самого большого файла проекта — на пять целевых модулей: базовые маршруты volume/mute/pause остались в api.py (581 строка); Bluetooth scan/pair/reconnect переехали в api_bt.py (396); интеграция с Music Assistant и OAuth-поток — в api_ma.py (1 216); config и настройки — в api_config.py (502); статус, SSE-стриминг и диагностика — в api_status.py (647). Каждый модуль регистрирует собственный Flask Blueprint; web_interface.py связывает все пять. Были добавлены обратно-совместимые реэкспорты, чтобы существующие тесты и внешние вызовы продолжали работать без изменений.

Потокобезопасность получила точечные фиксы: шесть мест, итерировавших глобальный список _clients без _clients_lock, были исправлены — три в ma_monitor.py через новый хелпер state.get_clients_snapshot(), два в config и MA routes. Счётчик MaMonitor._msg_id, ранее обычный int, инкрементируемый из разных потоков, был заменён на itertools.count(1) — атомарный в CPython. Дублирующее регулярное выражение MAC-адреса было консолидировано в services/bluetooth.py как каноническая функция is_valid_mac().

Все 138 тестов прошли после рефакторинга; ruff check оставался чистым на протяжении всей работы.