Все заметные изменения в этом проекте документируются здесь. Формат основан на Keep a Changelog, версионирование — SemVer.
camera.<name>_lyrics_na_tv: артист/заголовок теперь обновляются в караоке-стриме для Bluetooth и радио.artistвычислялся один раз до while-loop и оставался frozen. Цикл живёт, покаtrack_idнеизменен — а для BT/radio этоNone(либо stream-id) постоянно, при этом метаданные меняются с каждым треком. Итог:track.titleбрался свежий изcoordinator.trackи обновлялся, аartistзастревал. Перенёс вычисление внутрь loop послеtrack = self.coordinator.track(commit4577285).
- CI: добавлены
httpxиrespxвrequirements-test.txt(mock httpx-запросов в тестахlyrics_client/zvuk_client; исправляет красный CI, появившийся с 0.35.0). Отражено в CLAUDE.md. - i18n: убран URL из
service.play_music.url.descriptionвtranslations/{en,ru}.json— hassfest translations validation ругался на URL в описании (commitdde54c7). - CI:
actions/setup-python@v5 → v7(dependabot). - Docs:
sboom.play_music→sboom_ha.play_musicв README (правильный DOMAIN); добавлен сервис в таблицу «Сервисы»;httpx>=0.27внесён в раздел «Зависимости»; уточнён диапазон скорости0.5×–2.0×. - CLAUDE.md: перегенерированы таблицы
OP_*иMEDIA_CMD_*изconst.py, добавленыMEDIA_CMD_START_MULTIROOM=8,OP_FIND_REMOTE=13, BT-семействоOP 19-22,OP_SET_PLAYBACK_SPEED=23. Обновлена архитектурная диаграмма: 10 платформ вместо 7, добавлен NetEase и Zvuk CDN как внешние зависимости.
- Ошибка
customElements.define … "sboom-nowplaying" has already been usedпри открытии панели, когда на дашборде используется карточкаsboom-card. Карточка импортирует общие компоненты без?v, панель — с?v=<версия>; разные URL заставляли браузер выполнять модуль дважды, а незащищённыйdefineпадал. Все shared-компоненты (sboom-nowplaying,sboom-controls,sboom-browse,sboom-track-row) теперь регистрируются идемпотентно (if (!customElements.get(...))), какsboom-toast/sboom-tile— двойная загрузка безопасна.
- Рефактор фронтенда для переиспользования: data-feed логика панели (push-подписка
sboom/subscribe+ poll-fallback + devices/state/seek/toast-релей) вынесена в общий базовый классSboomFeedBase(www/components/sboom-feed-base.js); дизайн-токены--sb-*— вwww/components/sboom-tokens.css.js. Панель наследует базу без изменения поведения. Открывает путь для standalone Lovelace-карточки (sboom-card), переиспользующей те же компоненты и feed-слой. - Статика
/sboom_panelтеперь раздаётся всегда, независимо от опцииpanel_enabled— раздача www/ отвязана от регистрации боковой панели. Опция гейтит только пункт в боковом меню; общие компоненты остаются доступны для Lovelace-карточкиsboom-cardдаже при выключенной панели.
- Встроенная премиум-панель плеера в боковом меню Home Assistant («SberBoom»): immersive-карточка (обложка во весь фон, фрост-стекло, ambient-glow из цвета обложки, скраббер, транспорт) + люксовые SVG-контролы. Оформление на нативных токенах темы HA (палитра/акцент/радиусы адаптируются к теме пользователя). Отображает версию интеграции; отзывчива до мобильного; на простое схлопывается.
- Единый drill-down браузер список+поиск без табов: очередь → поиск (Исполнители/Альбомы/Треки/Плейлисты) → артист (аватар, топ-треки, дискография) → альбом (треклист) → трек. Навигационный стек, кнопка «назад», персистентный поиск.
- Богатый поиск и метаданные Sber Звук — обложки, дискография, треклисты; работает с домашнего IP без VPN. Backend: standalone
ZvukClient(get_tracks/get_artists/get_releases/get_artist/get_release, поиск, доминирующий цвет обложки) + WebSocket API панели. - Расширенные контролы: лайк/дизлайк (тюнинг персональной волны), скорость воспроизведения (0.5×–2.0×), найти пульт, shuffle/repeat, громкость.
- Поддержка нескольких колонок — селектор устройства; команды/подписка/очередь/поиск адресуются по
entry_id. - Опция включения/выключения панели (Settings → Integrations → SBoom → Configure → «Показывать панель плеера в боковом меню»).
- Сервис
sboom_ha.play_music— запуск по ссылке zvuk.com, поисковому запросу или id+kind.
- Новая рантайм-зависимость
httpx>=0.27вmanifest.json(добавлена радиZvukClientи миграцииlyrics_client). Устанавливается HA автоматически при обновлении.
- Мгновенные обновления панели (push-подписка на координатор вместо 5-сек поллинга); прогресс-бар якорится к серверному снимку и идёт плавно, корректен после перезагрузки.
- Оптимистичные кнопки транспорта (мгновенная реакция); дедуп play-команд; «потерянные/неверные» тапы устранены (shouldUpdate-гейт, захват трека на pointerdown, приручённый рефетч очереди) — по итогам глубокого аудита.
- Убран мусорный
playlist_titleиз now-playing.
- Matter-сенсор пропадал (
unavailable) при подключении реального Matter-устройства к колонке. Probeasync_matter_probeискал слово «matter» в выводеmatter list, но реальная таблица устройства (| NodeId | XID | Serial number | Model ID | … |) его не содержит → probe ложно возвращалFalse→has_matter_cli=False→ сенсор не пересоздавался при рестарте (становился restored/unavailable). Теперь probe определяет доступность по отсутствию error-вывода CLI (не по слову «matter»). matter listпарсится в структуру (node_id / serial / model / RSSI): сенсор показывает список устройств в атрибутах, а count больше не считает строку-заголовок за устройство. Формат подтверждён на реальном устройстве (MTFFF40002).
- Опция «Караоке-закраска» (Options Flow) — посимвольная подсветка строки по мере пропевания. По умолчанию выключена: строка показывается статично. Когда off — доля пропетости (
frac) не идёт ни в рендер, ни в кэш-ключ стрима, так что лишних кадров нет.
- Закраска рассчитана на письмо слева-направо; для RTL (арабский/иврит) направление заливки не инвертируется (нужны bidi + shaping, напр. libraqm) — отдельная задача. Т.к. закраска off по умолчанию, на поведение «из коробки» не влияет.
- Заглушка-фон вместо чёрного экрана, когда обложка не нашлась (редкий BT-трек, радио без арта, сбой загрузки). Бандл из 4 свободных градиентов (StockSnap/Flickr, CC0 1.0 — атрибуция не требуется, см.
backgrounds/CREDITS.txt). Выбор стабильно-случайный по треку (title|artist→ индекс) — фон не мигает от кадра к кадру, но у разных треков разный. Применяется и к караоке-кадру, и к idle. Общий вес ассетов ~64 КБ.
- Обложка для Bluetooth и радио по исполнителю+треку. У некаталожного контента нет Zvuk-
release_id, поэтому обложки не было (карточка и караоке-фон пустые). Теперь ищется поtitle+artistв публичных Search API без ключей: iTunes (апскейл до 600×600) → Deezer (резерв). Волатильно (один in-memory слот на трек, без диск-кэша). Подхватывается и вmedia_image_url(карточка media_player), и в blur-фон караоке-камеры. Радио тоже получает обложку текущей песни (обложке синк не нужен). - Кэш обложки в караоке-камере переведён с
track_idна URL — раньше некаталожные треки (безtrack_id) делили один ключ.
- Радио исключено из караоке. Колонка отдаёт для радио позицию ЭФИРА (напр. 1224 c при песне 36–250 c), а не песни — синхронизировать лирику нечем. Лирика для радио больше не запрашивается, караоке-камера показывает idle (обложка/заголовок), а не замороженную последнюю строку. Bluetooth (позиция песенная) — работает и синхронизируется корректно (подтверждено вживую).
- Диагностический сенсор
_lyricsбольше не показываетno_trackдля Bluetooth: теперь состояние лирики отражается и для некаталожного контента (у которого нет Zvuktrack_id).
- Караоке/лирика для Bluetooth и радио — тянется напрямую из провайдеров (Lrclib → NetEase), без персистентного диск-кэша. У BT/радио нет глобального каталожного
track_id(он есть только у контента Zvuk), поэтому:- ключ — синтетический
title|artists; кэш каталога (поtrack_id) не затрагивается и не засоряется; - один in-memory слот на текущий трек — API не дёргается каждый poll, но и на диск ничего не пишется (BT — почти всегда одноразовая чужая музыка, радио — эфемерно);
- контент без исполнителя (радио-реклама, джинглы) не запрашивается (гейт по
artists). - BT синхронизируется по позиции/длительности от колонки; у радио позиция эфирная, поэтому синк приблизительный (сам текст показывается).
- ключ — синтетический
- Радио и Bluetooth больше не показываются как IDLE с пустой карточкой. У радио и BT-аудио нет
trackId, поэтомуget_metadata(op 10) для них пуст → трек былNone→ media_player висел вIDLE, хотя звук идёт. Теперь now-playing для них добирается изGET_STATE:- Радио: станция →
media_channel(штатное поле HA), текущая песня →media_title, исполнитель →media_artist. На паузе остаётся станция, state =PAUSED(неIDLE). - Bluetooth (app
bluetooth_media_control, а неmusic): title/исполнитель/альбом изplayer.info;media_source="BLUETOOTH". - Прогресс-бар скрывается у радио (
duration=0→None); обычная музыка/волна/подкаст (у них естьtrackId) не затронуты.
- Радио: станция →
- Сенсор
sensor.<name>_playback_mode— тип воспроизведения:music/wave/podcast/radio/bluetooth. Единого поля-режима у колонки нет (radio поmode, wave/podcast поplaylistType, bluetooth по источнику) — сенсор их сводит и работает даже когда media_player пуст.
- Слой межустройственной связки в GET_STATE. Раньше парсили только
multiroom.mode/stereoPair.active; теперь читаем весь слой объединения устройств Sber:- Новый диагностический сенсор
sensor.<name>_device_links(выкл. по умолчанию) — число связанных устройств (SberCast + группы селектора + саундбар), разбивка по источникам в атрибутах. На одиночной колонке0; наполняется при объединении с SberBox/ТВ/другой колонкой (companion-протокол общий для платформы StarOS). - Сенсор
multiroom_modeобогащён атрибутами стереопары: канал (L/R) и устройство-партнёр (multiroom.stereoPair.channelFromConfig/pairDeviceFromConfig). - Парсинг
sbercast,deviceSelector(cast/ds/room/qc-группы),deviceGroups.soundBar— пустые источники отфильтровываются,""-каналы нормализуются вNone.
- Новый диагностический сенсор
- Честная документация op 18 (keep-alive). Эксперимент показал: транспортный WS ping/pong держит сессию сам (≥150 c idle без op 18 — соединение живо), а keep-alive-семантика op 18 нашим фаззингом не подтверждена. Комментарии в коде уточнены: op 18 оставлен как дешёвая application-level страховка, но его вклад в поддержание связи не изолирован. Поведение не менялось.
- Документация синхронизирована под аппаратные возможности: карточка HACS
info.md(датчики платы, инвентарь Zigbee/Matter) и дерево файлов README (iio_client.py,cli4242.py).
Аппаратные возможности колонки (только на моделях, которые их реально имеют — напр. SberBoom R2; проверяется при старте, иначе сущности не создаются):
- Сенсор освещённости (lux) и температуры SoC (°C) — с датчиков платы через libiio (:30431). Освещённость полезна для автоматизаций «стемнело → включи свет».
- Инвентарь Zigbee (
zigbee_inventory) — устройства умного дома, привязанные к колонке: количество + модели/производитель/RSSI/питание в атрибутах (через debug-CLI :4242). Только чтение инвентаря; состояние/управление CLI не даёт (идёт через облако Sber). - Инвентарь Matter (
matter_inventory) — количество Matter-устройств + сырой вывод в атрибуте (формат строки структурируется при появлении реального устройства).
Все клиенты graceful: закрытый порт / отсутствие датчика = «недоступно», без ошибок.
Ранее игнорировавшиеся данные GET_STATE (нулевой протокольный риск, схемы из живого захвата):
- Сенсор «Канал прошивки» (
device_segments, напр. OpenBeta) — понять, бета у вас или стабильная. Diagnostic. - Сенсор «Приложение на экране» (
current_app) — приложение на переднем плане, в отличие от «Активное приложение» (играющее). - Атрибуты существующих сенсоров: весь z-order стек приложений (
app_stack) у «Активное приложение»; смещение часового пояса у «Часовой пояс»;multi_profile/child_voice_explicitу «Возрастной режим»;from_showу бинарника «Утреннее шоу».
- Плашка источника на караоке-кадре — вверху приглушённым шрифтом показывается контекст «Персональная волна · Sber Звук» (название станции/плейлиста + провайдер) из тех же metadata-полей, что оживили Now Playing. И в караоке-режиме, и в idle.
helpers.provider_label/source_labelвынесены из media_player (дедупликацияapp_name).
- Богатый Now Playing — контекст трека в атрибутах
media_player:playlist(название станции/плейлиста, напр. «Персональная волна»),playlist_type(endless/album/user),media_source(MUSIC/RADIO/…),provider,playlist_liked,child_mode,buffering. Всё уже приходило в metadata колонки — раньше просто не выставлялось (аplaylistTitleи вовсе парсился, но терялся). Поля подтверждены живым захватом при переключении треков.
- Календарь перерисовывается только при реальном изменении расписания. Раньше CalendarEntity наследовала обновление от координатора и пересчитывала RRULE всех будильников на каждый poll и каждый push трека/громкости. Теперь сверяется отпечаток расписания (id + ics/intervalSec/reminderTime, без тикающего
timeLeftSec) — тик таймера и смена громкости больше не дёргают пересчёт календаря, а появление/удаление/редактирование события по-прежнему обновляет его сразу.
- Документация синхронизирована под 0.19: карточка HACS
info.mdи mermaid-диаграмма README дополнены Calendar-сущностью.
- Calendar-сущность «Расписание» — будильники, таймеры и напоминания колонки одним календарём (
calendar.<name>_schedule), синхронизируется с приложением «Салют». Будильники раскрываются из iCalendar RRULE (каждый день / будни / выбранные дни недели — корректный повтор), таймеры показываются событием до момента окончания, напоминания — по времени с заголовком. Категория события — в описании. Всё в UTC. Read-only (LAN-протокол не даёт op для создания событий). - Сенсоры «Следующий будильник» и «Следующий таймер» (
sensor.*_next_alarm/_next_timer, device_class timestamp) — время ближайшего срабатывания для карточек и автоматизаций.
Форматы подтверждены живым захватом GET_STATE с колонки; парсинг покрыт тестами на реальной фикстуре (tests/fixtures/alarm_state.json).
- Актуализирована документация под накопленные 0.14–0.18: README (диаграмма архитектуры с binary_sensor/device_tracker/lyrics_manager, цепочка Lrclib→NetEase, раздел про автоматизации) и карточка HACS
info.md(караоке-подсветка, скорость, BT, device tracker, device-триггеры/действия, диагностические сенсоры, авто-восстановление IP).
- Device-триггеры и device-действия — команды и события колонки в UI-конструкторе автоматизаций без YAML. Триггеры: сменился трек / изменилось воспроизведение / громкость / связь (обёртки над событиями
sboom_*, фильтр по конкретной колонке). Действия: play/pause/next/previous, обновить метаданные, найти пульт, Bluetooth-сопряжение. Чистый HA-слой, нового протокола не требуют. - Сенсор «Координаты» (диагностический, выключен по умолчанию) — сырые Wi-Fi-координаты колонки строкой
lat, lon, а lat/lon/accuracy/source в атрибутах. В отличие отdevice_tracker(state = имя зоны, «Дома»/«Не дома»), показывает именно координаты — для карточек и шаблонов.
device_tracker— местоположение колонки по Wi-Fi. Колонка сама сообщает свои координаты в GET_STATE (location{lat,lon,accuracy,source:"wifi"}), интеграция выставляет их GPS-трекером. Полезно для зон присутствия и карты. Выключен по умолчанию (координаты — чувствительные данные). Схема подтверждена реальными захватами.
Новые сущности из данных, которые колонка уже присылала в GET_STATE, но интеграция их не читала (все схемы подтверждены реальными захватами, нулевой протокольный риск):
- binary_sensor «Будильник звонит» (
alarm.playing, device_class sound) — момент физического срабатывания будильника/таймера на колонке. Готовый триггер для сценариев «проснулся будильник → включи свет/кофеварку». - binary_sensor «У трека есть текст» (
info.hasLyrics), «Автогромкость ассистента» (assistant.auto_volume), «Проактивное уведомление ассистента» (proactivityNotification). - Диагностические сенсоры (выключены по умолчанию): IP-адрес колонки (
network.ip), часовой пояс (time.timezone_id), возрастной режим профиля (user_settings.age_mode), рассинхрон часов колонки с Home Assistant (timesync.unixtime− now) — помогает диагностировать сдвиг караоке/позиции.
- Караоке рисуется напрямую текстом, без полнокадровых масок: белая строка + «пропетый» префикс акцентным цветом поверх (позиции глифов совпадают пиксель-в-пиксель). Символ загорается целиком — без разрезанных букв и артефактов маскирования по краям глифов; кадр подешевел с ~4 полнокадровых буферов до нуля (~9 мс на кадр — 20-кратный запас над 5 FPS).
- Караоке-заливка стала посимвольной (по фидбеку из прода). Раньше маска резалась одним вертикальным срезом по всему боксу — многострочные тексты закрашивались на всех экранных строках одновременно. Теперь прогресс строки распределяется по символам в порядке чтения: экранные строки закрашиваются последовательно, срез внутри строки считается по метрикам шрифта, а слово автоматически получает время пропорционально своей длине (длинные слова «поются» дольше).
- Позиция трека сбрасывалась к началу каждые 15 секунд (регрессия 0.13.0, найдена пользователем в проде). Poll-ответ
get_metadataнесёт позицию на момент последнего события (для недавно начатого трека — 0), а не текущую; свежий штамп времени получения на этом stale-снапшоте перезапускал экстраполяцию на каждом poll-цикле. Теперь база экстраполяции переносится, если payload несёт тот же снапшот позиции (track_id + tsMs + position не менялись); новое событие (seek/пауза/смена трека) по-прежнему даёт свежий штамп.
- Служебные кредит-строки NetEase вычищаются из текста. LRC от NetEase начинается со строк вида
[00:00.00] 作曲 : <композитор>— без фильтра кредит висел в караоке до первой настоящей строки (найдено живым тестом на «Всё идёт по плану» Егора Летова). Фильтруется сам synced-текст, чтобы кредиты не возвращались из персист-кэша после рестарта.
- Резервный источник текстов — NetEase Cloud Music. Если Lrclib не нашёл текст (или нашёл только plain без таймстампов), интеграция пробует NetEase: поиск трека с матчингом по названию/артисту/длительности (±7 с — чтобы не подцепить текст remix/live-версии), затем LRC-текст. Приоритет всегда у synced-текста. Отключается опцией
lyrics_netease_fallback. Источник текста виден в атрибутеsourcelyrics-сенсоров.
Большой релиз по итогам глубокого код-ревью (85 подтверждённых находок): надёжность соединения, honest-соответствие HA Integration Quality Scale, плавное караоке, CI и масштабное покрытие тестами.
- Half-open соединения теперь детектируются: включён WS ping/pong + принудительный reconnect после N подряд неудачных poll-циклов. Раньше обесточенная колонка выглядела «доступной» десятки минут.
- Reconnect-backoff сбрасывается только после стабильной сессии — flapping-соединение больше не даёт вечный tight-loop с вечно available entities.
connected=Trueтолько после первого успешного запроса — колонка, принимающая WS, но не отвечающая, больше не считается подключённой.- Битый/частичный state-push не обнуляет громкость и device-сенсоры: parse_state возвращает None при провале разбора, недостающие поля домердживаются из прежнего состояния.
- Позиция трека и караоке не зависят от часов колонки — экстраполяция от момента получения данных (monotonic) с учётом скорости воспроизведения; clock skew больше не сдвигает лирику.
volume_up/volume_downаккумулируются — optimistic-обновления состояния после команд (volume/mute/play/pause/shuffle).- Push-события больше не откладывают volume-poll и не отменяют подтверждение команд; устранена двойная перерисовка всех entities на каждый poll.
- Healing-миграция unique_id не захватывает entry чужой колонки при переиспользовании IP (проверка device_id).
- Порядок unload исправлен (сначала платформы, потом координатор); cleanup при ошибке setup; фоновые lyrics-задачи привязаны к entry; кэш лирики удаляется вместе с entry.
- Pair-токен больше не пишется в лог открытым текстом;
client_hostредактируется в diagnostics. - TLV-декодер: wire-type 5 (fixed32) больше не обрывает молча разбор сообщения.
- Плавное караоке: заливка текущей строки акцентным цветом по мере пропевания (~5 FPS), прогресс-бар и таймер обновляются каждую секунду, повторяющиеся строки припева больше не замораживают кадр; кэш blur-фона и шрифтов; snapshot камеры рендерит lyrics-кадр и уважает запрошенный размер.
- Настройка сдвига лирики (−10…+10 с) в Options; тик сенсора текущей строки планируется на границу следующей строки.
- LRC: поддержка multi-timestamp строк и enhanced-LRC word-тегов.
- Repairs fix-flow мигрирует legacy unique_id при смене host; сервисы получили schema-валидацию и
ServiceValidationErrorвместо молчаливого игнора. - CI: pytest (Python 3.11–3.13) и ruff на каждый push/PR, dependabot, минимальные permissions.
- Локализация сервисов через translations, шаг reauth в переводах,
NumberSelectorс единицами в Options,device_classдля media_player (speaker) и lyrics-сенсора (enum).
- Соответствие IQS: сервисы регистрируются в
async_setup(action-setup), координатор вentry.runtime_data, логи доступности только на переходах (log-when-unavailable),quality_scale.yamlсинхронизирован с реальностью. - Рефакторинг SOLID/DRY: lyrics-менеджер выделен из координатора, 14 boilerplate-классов сенсоров → декларативные спеки, дедупликация pair-handshake, единый repeat-маппинг, именованные константы протокола.
- Треки без synced-текста дают
state=Noneу сенсора строки вместоunavailable.
Устойчивость к смене IP-адреса колонки.
- Fixable Repairs-issue — когда колонка недоступна > 5 минут, из issue в Settings → System → Repairs теперь можно сразу ввести новый IP: соединение проверяется, entry обновляется и перезагружается. Pair-токен и device_id сохраняются, повторное сопряжение не требуется.
- Дубликаты при ручном добавлении. Entry, добавленный вручную по IP, получал unique_id, привязанный к адресу, и не матчился с zeroconf-discovery. После смены IP Home Assistant предлагал «новое устройство», а подтверждение создавало дубликат с повторным pairing. Теперь при первом же discovery такой entry автоматически мигрирует на unique_id по device_id — дальше смена IP лечится штатной soft-migration (host/port обновляются, интеграция перезагружается).
- Ручной ввод IP уже настроенной колонки больше не создаёт второй entry — flow прерывается с
already_configuredпо совпадению host. - Устаревший unique_id после reconfigure. Для entry с host-based unique_id смена адреса через Reconfigure обновляет и unique_id — старый IP больше не «занят» и не блокирует добавление другой колонки на нём.
- Сенсор «Активное приложение» флапал каждый poll-цикл, перебирая
music/news/bluetooth_media_control/… Причина: значение бралось изbackground_apps[0], а это самотасующийся z-order стек — первый элемент меняется сам по себе. Теперьactive_app— приложение с реально активным плеером (state.player.playing); если ничего не играет — состояниеunknown.
Управление Bluetooth: сопряжение, спаренные устройства, поиск пульта.
- Кнопка «Bluetooth-сопряжение» — переводит колонку в режим сопряжения по Bluetooth. Длительность окна видимости задаёт прошивка колонки.
- Кнопка «Найти пульт» — команда поиска пульта ДУ.
- Сенсор «Спаренные Bluetooth-устройства» — количество + список (MAC, имя, статус подключения) в атрибутах.
- Сервис
sboom_ha.bluetooth_device— подключить / отключить / удалить спаренное BT-устройство по MAC. - Методы клиента:
find_remote(),bt_make_discoverable(),get_paired_bt_devices(),get_scanned_bt_devices(),bt_device_command(). - TLV-кодек:
decode_repeated()— декод с поддержкой повторяющихся тегов (для списков устройств).
Персист lyrics-кеша + парсер-слой очереди воспроизведения.
- Персист lyrics-кеша.
lyrics_to_dict()/lyrics_from_dict()+ HAStore(JSON в.storage/). Кеш текстов песен загружается при старте интеграции и сохраняется с debounce — раньше жил только в памяти и терялся при каждом рестарте HA. - Парсер-слой очереди воспроизведения (
op=17):QueueTrack,parse_queue(),SberSpeakerClient.get_queue()возвращаетlist[QueueTrack]. Низкоуровневая утилита без UI-сущности — очередь колонки отдаёт толькоtrackIdбез названий, а резолв через Zvuk API требует авторизации (отдаёт401без токена).
- Lyrics-сенсор
_tickвызывалasync_write_ha_state()из executor-потока (RuntimeErrorв логе на HA 2026.5 / Python 3.14). Добавлен декоратор@callback— таймер-колбэк теперь исполняется в event-loop.
Сенсоры подсистем устройства из GET_STATE.
- Платформа
binary_sensor— новая, 6 сущностей. - 13 read-only сущностей из уже приходящего
GET_STATE(раньше из него брались толькоvolume/muted):sensor: яркость дисплея (%), будильники, таймеры, активное приложение, персона ассистента;multiroom-режим и тип подключения — diagnostic.binary_sensor: дисплей включён, колонка активна (device_class: running), стереопара; устройство подписки, домашняя безопасность, утреннее шоу — diagnostic.
DeviceState— модель подсистем устройства;SpeakerState.device— поле с ней.parse_device_state()— парсер подсистемGET_STATE.
parse_state()теперь извлекает сбалансированный JSON-объект и парсит подсистемы устройства. При битом JSON — fallback на прежний regex поvolume,device=None.
Управление скоростью воспроизведения.
- Select-сущность «Скорость воспроизведения» (
select.*_playback_speed). Пресеты 0.5×–2.0× с шагом 0.25. Скрыта на dashboard «Auto» по умолчанию (entity_registry_visible_default = False) — как и остальные side-feature сущности. SberSpeakerClient.set_playback_speed()— командаop=23(OP_SET_PLAYBACK_SPEED). Скорость кодируется как float (TLV wire-type 5, 4 байта LE IEEE-754); varint и nested-JSON ломаютplaybackSpeedRateколонки в0.0(подтверждено вresearch/exp_22). Значение жёстко ограничивается диапазоном 0.5–2.0.TrackInfo.playback_speed—playbackSpeedRateиз метаданных трека (push- и state-формат).- TLV-кодек (
_tlv.field) получил поддержкуkind=5(fixed32 float).
Правки по итогам code-аудита всей интеграции.
ConfigEntryNotReadyпри недоступной колонке на старте. Раньше первый connect шёл в фоне — интеграция «поднималась» в заведомо мёртвом состоянии, даже если колонка офлайн. Теперь первый connect выполняется синхронно вasync_setup_entry; при неудаче HA откладывает и повторяет setup. Дальнейшие реконнекты по-прежнему ведёт фоновый supervisor.- Команды сущностей оборачивают транспортные ошибки в
HomeAssistantError. При мёртвом WS медиа-команды (media_player,button,switch,number,select) раньше всплывали в UI сырымRuntimeError/ConnectionErrorс traceback'ом. Теперь — понятное переводимое сообщение. - MJPEG-стрим камеры устойчив к ошибкам рендера. Исключение PIL/отрисовки внутри stream-loop больше не роняет весь поток с HTTP 500 — кадр логируется и поток продолжается.
pair_with_buttonотклоняет вызов при активном listen-loop (раньше они молча конкурировали бы заrecv()).- Reconfigure-flow проверяет соединение с новым host/port перед сохранением — опечатка в IP больше не уводит entry в вечно-битое состояние, показывается инлайн-ошибка.
DataUpdateCoordinatorтеперь получаетconfig_entryявно (устраняет deprecation, ломавшийся бы в HA 2026.8).PARALLEL_UPDATESобъявлен на всех платформах.- Чистка мёртвого кода в
api.py: убраны неиспользуемыеKEEPALIVE_INTERVAL_SEC, поле_keepalive_task; импорты подняты в шапку модуля. quality_scale.yamlприведён в соответствие реальности (action-exceptions,parallel-updates→ done).
- Тесты герметичны. Если в окружении установлен настоящий пакет
homeassistant(например, venv соседнего проекта), он перехватывал импорты вместо stub'ов — тесты падали с невнятнымFrame helper not set up. Теперьinstall_stubs()распознаёт ситуацию и падает сразу с понятной инструкцией про чистый venv. - Добавлен
test_audit_fixes.py(143 теста суммарно): обёртка команд, поведение poll при обрыве, отмена pending-запросов в listen-loop, устойчивость_handle_event.
- Шумные ERROR-traceback'и при штатном обрыве связи. Когда колонка сбрасывала WS-соединение (перезагрузка, Wi-Fi-мигание, idle-disconnect), интеграция за один обрыв писала ~56 ERROR-traceback'ов:
listen loop crashed+ по дваget_state failed/get_metadata failedна каждый poll-цикл в окне до reconnect. Функционально всё восстанавливалось, но лог захламлялся.api._listen_loopтеперь отличает штатныйConnectionClosed/OSError(→ одинINFOбез traceback) от настоящего сбоя (→ERROR).- При выходе из listen-loop выставляется
disconnected-событие — супервизор реагирует на разрыв мгновенно, не дожидаясь следующего keepalive-цикла (было до ~25s задержки). - Ожидающие запросы при обрыве немедленно отменяются вместо 5-секундного таймаута.
coordinatorпропускает poll, пока соединение в обрыве, и логирует in-flight сбой компактнымWARNINGвместо полного traceback.
- Default
volume_poll_intervalподнят с 5s до 15s. Эмпирически подтверждено в research-сессии (см.research/PROTOCOL.md): метаданные трека приходят push-events через subscribe-stream (активируется первымget_metadata-запросом). Polling нужен только для volume/mute, которые в push-stream НЕ попадают. 15s — разумный компромисс. Юзер может настроить через Options Flow если хочет иначе. - В supervisor добавлен комментарий объясняющий subscribe-механизм.
- Существующие установки с явно установленным
volume_poll_intervalв options — продолжат работать с тем значением что юзер выбрал. Только default-значение изменилось.
- Hassfest validation:
services.yamlисправлен —target.device.integrationушёл, вместо негоfields.device_id.selector.device.integration. Тот же UX, корректный формат.
Большое обновление жизненного цикла интеграции — закрыли почти все Bronze/Silver правила HA Quality Scale.
- Diagnostics —
Settings → Devices → SBoom → ⋮ → Download diagnosticsотдаёт JSON со state coordinator, track/state, options и entry.data (с redaction токена/host/serial). - Options Flow —
Settings → Integrations → SBoom → Configureдля тонкой настройки:volume_poll_interval,keepalive_interval,availability_threshold,lyrics_enabled. Авто-reload при изменении. - Reconfigure Flow — изменить IP/порт уже привязанной колонки без удаления интеграции (через ⋮ меню в Devices). PIN-токен сохраняется.
- Reauth Flow — переавторизация без удаления entry. Запускается через service
sboom_ha.reauthили будет триггериться автоматически когда выяснится сигнал колонки на invalid token. - Repairs platform — issue
speaker_unreachableсоздаётся при недоступности колонки > 5 минут, удаляется при reconnect. Видно вSettings → System → Repairs. - System Health — метрики
configured_speakers,connected_speakers,lrclib_reachableвSettings → System → System Information. - Custom services:
sboom_ha.refresh_metadata(force-poll без ожидания цикла),sboom_ha.reauth(запустить переавторизацию). Поддержка таргетинга по device_id. zeroconf reload_on_update=True— при смене IP колонки через mDNS-rediscover интеграция перезагружается автоматически, client начинает ходить на новый адрес.async_migrate_entry+MINOR_VERSION=1— заглушка миграций config entry для будущих изменений формата.quality_scale: silverв manifest +quality_scale.yamlс пометкамиdone/todo/exemptдля движения к Gold.
- Хардкоды → опции:
volume_poll_interval(5s),keepalive_interval(25s),availability_threshold(3) перенесены из const.py в options. Дефолты сохранены. - Тестов: 105 → 135 (+30). Добавлены
test_diagnostics,test_options,test_repairs,test_services,test_system_health. - Stubs HA: расширены до полной поддержки
issue_registry,system_health,services,device_registry.async_get,ServiceCall,RepairsFlow,ConfirmRepairFlow.
- Brand-ассеты перенесены в правильное место: HA 2026.3+ использует Brands Proxy API — иконки кладутся в
custom_components/{domain}/brand/, аhome-assistant/brandsрепо больше не принимает PR для custom integrations. Папкаbrand-assets/(не туда положенная в 0.6.5) удалена, файлы переехали вcustom_components/sboom_ha/brand/{icon,icon@2x}.png. - Иконка теперь автоматически отображается в HA UI через
/api/brands/integration/sboom_ha/icon.png— никаких сторонних PR не требуется.
- Brand-ассеты для HA UI:
icon.png(256×256) иicon@2x.png(512×512), заимствованные изcustom_integrations/sberdevicesрепо home-assistant/brands. - Изначально файлы были в
brand-assets/, в 0.6.6 переехали в правильное местоcustom_components/sboom_ha/brand/.
- Индикация недоступности колонки: все entity (
media_player,camera,sensor,button,switch,number,select) теперь показываются какUnavailableесли WS-сессия с колонкой мертва. Реализовано черезSboomCoordinator.connected+SboomEntity.available. - Event bus:
sboom_connection_changed(поляconnected,entry_id,device_id,host) — для автоматизаций «колонка отвалилась → push в Telegram» и подобных.
- Debounce доступности (3 неудачные попытки): чтобы транзиентные мерцания сети не дёргали entity в
Unavailableи обратно, флагconnectedпереключается вFalseтолько после 3 подряд неудачных reconnect'ов (DISCONNECT_THRESHOLD). Успешный connect мгновенно возвращает entity в available-состояние. - При
connected-flip принудительно вызываетсяasync_update_listeners()— UI перерисовывается мгновенно, без ожидания следующего poll-цикла.
- CI: обновлены GitHub Actions до Node.js 24-совместимых версий —
actions/checkout@v4 → @v6,actions/setup-python@v5 → @v6. Убрано предупреждение о deprecated Node.js 20 (forced removal 16 сентября 2026).
- Hassfest validation: ключи в
manifest.jsonпересортированы (domain,name, далее алфавит) — требование Home Assistant для прохождения CI.
- CI: убран
hacs/actionjob — для приватного репо он бессмысленен (action ходит через публичный API без аутентификации). Hassfest и unit-тесты остались. Если репо когда-нибудь перейдёт в public и появится цель публиковаться через HACS-store — вернуть job обратно (см. git history) и добавить brand assets.
- Расширенный DeviceInfo в карточке устройства HA: firmware (
sw_version) из mDNS-broadcast'а колонки и серийный номер (serial_number = device_id). Теперь видно прошивку и серийный сразу в Settings → Devices. - Soft-migration для существующих entries: при каждом zeroconf-rediscover (колонка регулярно сама анонсирует mDNS) firmware и model автоматически обновляются — старые установки получат firmware без переустановки.
- Firmware из mDNS извлекался при discovery, но не сохранялся в config entry — баг исправлен.
- HA event bus — интеграция выпускает события
sboom_track_changed,sboom_playback_changed,sboom_volume_changedпри изменениях на колонке. Триггерится черезplatform: eventв автоматизациях. См. README раздел «События для автоматизаций».
- Рефакторинг
api.py— выделены модули_tlv.py(бинарный кодек),_parsers.py(JSON-парсеры payload'ов),_models.py(dataclassesTrackInfo/SpeakerState).api.pyуменьшился с 692 до 399 строк. Публичный API сохранён через re-export вSberSpeakerClient.parse_track/parse_state. - Тестовая инфраструктура — добавлены
tests/_ha_stubs.py(легковесные HA-stubs) иtests/_fakes.py(builders coordinator/track/state). Покрытие выросло с 48 до 94 unit-тестов: добавлены тестыmedia_player(entity properties),_tlv(изолированный кодек),coordinator_events(event bus).
pyproject.tomlс конфигом для pytest и ruff (без[project]— версия остаётся вmanifest.json).
- Дополнительные сущности скрыты на dashboard по умолчанию (
entity_registry_visible_default = False):- 7 buttons (next/prev/play_pause/like/dislike/remove_like/remove_dislike)
- 1 number (volume)
- 2 switches (shuffle, mute)
- 1 select (repeat mode)
- Эти entities остаются enabled (работают в автоматизациях, services, шаблонах) — просто не загромождают dashboard "Auto" / overview-карточки
- Главные видимые entities:
media_player+sensor.lyrics_current_line - Чтобы вернуть на dashboard: Settings → Devices → SBoom → нужная entity → toggle "Visible"
- 48 unit-тестов:
tests/test_helpers.py,test_lyrics_client.py,test_api_parsers.py,test_image_render.py - CI workflow для запуска тестов на каждом push (GitHub Actions)
- Синтетические фикстуры payload-форматов (push + state-обёртка)
- Вынесены общие утилиты в
helpers.py(track_position,cover_url) — устранено дублирование междуsensor.py,camera.py,media_player.py SboomMediaPlayerтеперь наследует от общегоSboomEntity(DRY device_info)device_infoиспользуетDeviceInfodataclass вместо raw dict- Все вызовы
asyncio.get_event_loop()заменены наasyncio.get_running_loop() aiohttp.ClientTimeoutвместоintвcamera._fetch_cover_raw(совместимость с aiohttp 4.x)- Публичные методы
media_remove_like(),media_remove_dislike(),parse_track(),parse_state()вместо вызова приватных - Использование
MEDIA_CMD_*констант вapi.pyвместо magic numbers - Публичное свойство
coordinator.http_sessionвместо обращения к_http - Исправлен двойной
client.close()вconfig_flow.pypair-flow
- Tighter polling:
update_interval5s (было 30s) — громкость подтягивается быстрее - Прогресс-бар на camera — тонкая полоса + время
MM:SS / MM:SSв стиле Яндекс.Музыки - Lyrics retry с fallback по
artist+titleесли сalbum+durationне найдено
- Lrclib network errors теперь не кэшируются как "не найдено" — следующий push даёт retry
- Camera lyrics-overlay в стиле Яндекс.Музыки: blur-обложка + два уровня lyrics + footer с title/artist
- Idle-режим с большой обложкой по центру + подписи
- Camera entity
camera.<name>_lyrics_na_tvдля karaoke-стрима - MJPEG-стрим с двумя строками lyrics (текущая + следующая) с переключением по таймингу LRC
- Snapshot-mode для предпросмотра в HA UI
- Возможность отправки на ТВ через
media_player.play_media - Зависимость Pillow для PIL-рендера, шрифт DejaVuSans включён в пакет
- Lyrics через Lrclib.net (open API, без auth, синхронизированные LRC)
sensor.<name>_lyrics_current_line— текущая строка (тикает 1 Гц, state пишется только при смене)sensor.<name>_lyrics(диагностический, включается вручную) — полный текст в attributes- Кэш lyrics в coordinator с фоновым fetch при смене трека
- Добавлено
media_content_type=MUSIC— теперь HA more-info показывает артиста в secondary-строке вместоapp_name
- Auto-discovery через Zeroconf (
_staros._tcp.local.) - MediaPlayer с volume/mute/play/pause/next/prev/seek/shuffle/repeat
- Cover image из public Zvuk CDN (
https://cdn-image.zvuk.com/pic) - Sub-platforms: button (like/dislike/transport), number (volume), switch (shuffle/mute), select (repeat)
- Первичная реализация через PIN_AUTH WebSocket-сессию
- Pair-flow с нажатием
+на колонке - Базовый media_player entity