Yoradio integracja z Home Assistant

Witam.
Mam zintegrowane z Home Assistant radio internetowe Yoradio GitHub - e2002/yoradio: Web-radio based on ESP32-audioI2S library działa wyśmienicie po MQTT chciałbym dodać jeszcze cztery takie radia ale nie mam na tyle wiedzy jak to zrobić i jak zrobić encje do opcji odtwarzacza celem automatyzacji
dzięki za wszelką pomoc

Projekt wygląda fajnie (już kiedyś go gdzieś widziałem, ale wtedy mi się nie walało tyle niewykorzystanego sprzętu po szufladach, może kiedyś wprowadzę go do realizacji, chociaż nie jest mi tak naprawdę do niczego potrzebny).
A więc…
Czytałem, czytałem i znalazłem instrukcję (poniższe na jej bazie, bo raczej nie zbuduję joradio w najbliższych dniach)
https://github.com/e2002/yoradio?tab=readme-ov-file#home-assistant

Oprócz brokera MQTT są potrzebne

  1. integracja MQTT w HA (zapewne bez żadnych udziwnień) oraz zapewne plugin MQTT dla samego yoradio GitHub - e2002/yoradio: Web-radio based on ESP32-audioI2S library
  2. komponent niestandardowy stąd yoradio/HA/custom_components at main · e2002/yoradio · GitHub niestety nie można go dodać przez HACS, więc pozostaje ręczne skopiowanie katalogu yoradio wraz z jego zawartością we właściwe miejsce tj. /homeassistant/custom_components/ ewentualnie (zależy skąd widzisz mapowanie katalogów/udziałów) /config/custom_components/
  3. oraz odpowiednia konfiguracja YAML (po instalacji 2. należy zrestartować HA i dopiero wtedy dopisywać cokolwiek do configuration.yaml należącego do HA) oczywiście po jej ustawieniu jest konieczny kolejny restart HA (warto sprawdzić poprawność konfiguracji przed tym ruchem)
  4. dodać dowolną kartę media-playera wbudowaną lub niestandardową (moja ulubiona to dostępna w HACS GitHub - kalkih/mini-media-player: Minimalistic media card for Home Assistant Lovelace UI )

Gdyby ktoś szukał listy stacji z karadio wersje z roku 2019 i starsze
https://web.archive.org/web/20190714201852/http://karadio.karawin.fr/WebStations.txt
wersja z 2021

pokaż co masz i jak wygląda encja obecnego media-playera w narzędziach deweloperskich (może wystarczy nieco zmodyfikować co nieco w kolejnych egzemplarzach) kombinowałbym chyba ze zmianą tematu MQTT
z tego co widzę default to yoradio/100/ więc może yoradio/101/ itd.?
masz rękach to możesz ekserymentować

encja wygląda tak:

source_list:
  - 1. RMF FM
  - 2. RMF MAXXX
  - 3. RMF 30 LAT
  - 4. RMF 5 Lagodne Przeboje
  - 5. RMF 24
  - 6. Zlote przeboje nowy sącz
  - 7. Radio ZET
  - 8. Radio ZET 2000
  - 9. Radio ZET 80s
  - 10. Radio ZET 90s
  - 11. Radio ZET Dance
  - 12. Radio ZET Hits
  - 13. Radio ZET Party
  - 14. Radio ZET Polskie
  - 15. ESKA
  - 16. ESKA2
  - 17. ESKA IMPRESKA
  - 18. ESKA Gorąca 20
  - 19. ESKA ROCK
  - 20. ESKA Hity Na Czasie
  - 21. Radio TOK FM
  - 22. Radio Alex Zakopane
  - 23. Radio Plus Krakow
  - 24. Polskie Radio PR 1
  - 25. Polskie Radio PR 2
  - 26. Polskie Radio PR 3
  - 27. Polskie Radio PR 4
  - 28. Vox FM
  - 29. Vox FM Best Lista
  - 30. Radio FEST
  - 31. Radio Rekord
  - 32. Radio Top80
  - 33. 80s80s
  - 34. 80s80s Italo Disco
  - 35. 80s80s Maxis
  - 36. 80s80s in The Mix
  - 37. 90s90s In The Mix
  - 38. 90s90s Dance
  - 39. 90s90s Techno
  - 40. 2000er
  - 41. 1000 2000er
  - 42. Energy2000
  - 43. 1Mix Radio-Trance
  - 44. Radio 500
  - 45. BOBs Death Metal
  - 46. Rock Antenne
  - 47. Antyradio
  - 48. The Rock FM
volume_level: 1
media_title: Masterboy - Show Me Colours
media_artist: 90s90s Dance
media_album_name: ""
source: 38. 90s90s Dance
friendly_name: YoRadio
supported_features: 155573

zmiana z yoradio/100/ na yoradio/101/ nic nie wnosi za to zmiana wpisu przed ukośnikiem już tak.
Ustawiłem na jednym radiu kuchnia/100/ a na drugim salon/100/ i w configuration.yaml według pliku README zrobiłem wpisy:

# yoradio entity
media_player:
  - platform: yoradio
    name: YoRadio
    root_topic: salon/100

i teraz mam kontrolę nad radiem salon/100/ i teraz jak zmienię w configuration.yaml z root_topic: salon/100 na root_topic: kuchnia/100 i po restarcie mam kontrolę nad drugim i tu jest problem bo albo jedno albo drugie próbowałem rozmaitych wpisów ale zawsze są błędne.

Dodam że w custom_components trzeba było wstawić katalog yoradio z trzema plikami:
init.py który jest pusty
manifest.json

{
    "domain": "yoradio",
    "name": "ёRadio",
    "documentation": "https://github.com/e2002/yoradio",
    "dependencies": ["http", "mqtt"],
    "codeowners": ["@e2002"],
    "requirements": [],
    "version": "0.9.410"
  }

media_player.py

import logging
import voluptuous as vol
import json
import urllib.request
import asyncio

from homeassistant.components import mqtt, media_source
from homeassistant.components.media_player.browse_media import async_process_play_media_url
from homeassistant.const import CONF_NAME
from homeassistant.helpers import config_validation as cv

from homeassistant.components.media_player import (
    PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA,
    BrowseMedia,
    MediaPlayerEntity,
    MediaPlayerEntityFeature,
    MediaPlayerState,
    MediaPlayerEnqueue,
    MediaType,
    RepeatMode,
)

VERSION = '0.9.410'

_LOGGER      = logging.getLogger(__name__)

SUPPORT_YORADIO = (
    MediaPlayerEntityFeature.PAUSE
    | MediaPlayerEntityFeature.PLAY
    | MediaPlayerEntityFeature.STOP
    | MediaPlayerEntityFeature.VOLUME_SET
    | MediaPlayerEntityFeature.VOLUME_STEP
    | MediaPlayerEntityFeature.TURN_OFF
    | MediaPlayerEntityFeature.TURN_ON
    
    | MediaPlayerEntityFeature.PREVIOUS_TRACK
    | MediaPlayerEntityFeature.NEXT_TRACK
    | MediaPlayerEntityFeature.SELECT_SOURCE
    | MediaPlayerEntityFeature.BROWSE_MEDIA
    | MediaPlayerEntityFeature.PLAY_MEDIA
)

DEFAULT_NAME = 'yoRadio'
CONF_MAX_VOLUME = 'max_volume'
CONF_ROOT_TOPIC = 'root_topic'

MEDIA_PLAYER_PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend({
  vol.Required(CONF_ROOT_TOPIC, default="yoradio"): cv.string,
  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
  vol.Optional(CONF_MAX_VOLUME, default='254'): cv.string
})

def setup_platform(hass, config, add_devices, discovery_info=None):
  root_topic = config.get(CONF_ROOT_TOPIC)
  name = config.get(CONF_NAME)
  max_volume = int(config.get(CONF_MAX_VOLUME, 254))
  playlist = []
  api = yoradioApi(root_topic, hass, playlist)
  add_devices([yoradioDevice(name, max_volume, api)], True)

class yoradioApi():
  def __init__(self, root_topic, hass, playlist):
    self.hass = hass
    self.mqtt = mqtt
    self.root_topic = root_topic
    self.playlist = playlist
    self.playlisturl = ""

  async def set_command(self, command):
    try:
      self.mqtt.async_publish(self.root_topic + '/command', command)
    except:
      await self.mqtt.async_publish(self.hass, self.root_topic + '/command', command)

  async def set_volume(self, volume):
    command = "vol " + str(volume)
    try:
      self.mqtt.async_publish(self.root_topic + '/command', command)
    except:
      await self.mqtt.async_publish(self.hass, self.root_topic + '/command', command)
      
  def fetch_data(self):
    try:
      html = urllib.request.urlopen(self.playlisturl).read().decode("utf-8")
      return str(html)
    except Exception as e:
      _LOGGER.error(f"Unable to fetch playlist from {self.playlisturl}: " + str(e))
      return ""
        
  async def set_source(self, source):
    number = source.split('.')
    command = "play " + number[0]
    try:
      self.mqtt.async_publish(self.root_topic + '/command', command)
    except:
      await self.mqtt.async_publish(self.hass, self.root_topic + '/command', command)

  async def set_browse_media(self, media_content_id):
    try:
      self.mqtt.async_publish(self.root_topic + '/command', media_content_id)
    except:
      await self.mqtt.async_publish(self.hass, self.root_topic + '/command', media_content_id)
      
  async def load_playlist(self, msg):
    try:
      self.playlisturl = msg.payload
      file = await self.hass.async_add_executor_job(self.fetch_data)
    except uException as e:
      _LOGGER.error(f"Error load_playlist from {self.playlisturl}")
    else:
      file = file.split('\n')
      counter = 1
      self.playlist.clear()
      for line in file:
        res = line.split('\t')
        if res[0] != "":
          station = str(counter) + '. ' + res[0]
          self.playlist.append(station)
          counter=counter+1

class yoradioDevice(MediaPlayerEntity):
  def __init__(self, name, max_volume, api):
    self._name = name
    self.api = api
    self._state = MediaPlayerState.OFF
    self._current_source = None
    self._media_title = ''
    self._track_artist = ''
    self._track_album_name = ''
    self._volume = 0
    self._max_volume = max_volume

  async def async_added_to_hass(self):
    await asyncio.sleep(5)
    await mqtt.async_subscribe(self.api.hass, self.api.root_topic+'/status', self.status_listener, 0, "utf-8")
    await mqtt.async_subscribe(self.api.hass, self.api.root_topic+'/playlist', self.playlist_listener, 0, "utf-8")
    await mqtt.async_subscribe(self.api.hass, self.api.root_topic+'/volume', self.volume_listener, 0, "utf-8")
    
  async def status_listener(self, msg):
    js = json.loads(msg.payload)
    self._media_title = js['title']
    self._track_artist = js['name']
    if js['on']==1:
      self._state = MediaPlayerState.PLAYING if js['status']==1 else MediaPlayerState.IDLE
    else:
      self._state = MediaPlayerState.PLAYING if js['status']==1 else MediaPlayerState.OFF
    self._current_source = str(js['station']) + '. ' + js['name']
    try:
      self.async_schedule_update_ha_state()
    except:
      pass

  async def playlist_listener(self, msg):
    await self.api.load_playlist(msg)
    try:
      self.async_schedule_update_ha_state()
    except:
      pass

  async def volume_listener(self, msg):
    self._volume = int(msg.payload) / self._max_volume
    try:
      self.async_schedule_update_ha_state()
    except:
      pass

  @property
  def supported_features(self):
    return SUPPORT_YORADIO

  @property
  def name(self):
    return self._name

  @property
  def media_title(self):
    return self._media_title

  @property
  def media_artist(self):
    return self._track_artist

  @property
  def media_album_name(self):
    return self._track_album_name

  @property
  def state(self):
    return self._state

  @property
  def volume_level(self):
    return self._volume

  async def async_set_volume_level(self, volume):
    await self.api.set_volume(round(volume * self._max_volume,1))

  @property
  def source(self):
    return self._current_source

  @property
  def source_list(self):
    return self.api.playlist

  async def async_browse_media(
    self, media_content_type: str | None = None, media_content_id: str | None = None
  ) -> BrowseMedia:
    return await media_source.async_browse_media(
      self.hass,
      media_content_id,
    )

  async def async_play_media(
    self,
    media_type: str,
    media_id: str,
    enqueue: MediaPlayerEnqueue | None = None,
    announce: bool | None = None, **kwargs
  ) -> None:
    if media_source.is_media_source_id(media_id):
      media_type = MediaType.URL
      play_item = await media_source.async_resolve_media(self.hass, media_id, self.entity_id)
      media_id = async_process_play_media_url(self.hass, play_item.url)
    await self.api.set_browse_media(media_id)
    
  async def async_select_source(self, source):
    await self.api.set_source(source)
    self._current_source = source

  async def async_volume_up(self):
      newVol = float(self._volume) + 0.05
      await self.async_set_volume_level(newVol)
      self._volume = newVol

  async def async_volume_down(self):
      newVol = float(self._volume) - 0.05
      await self.async_set_volume_level(newVol)
      self._volume = newVol

  async def async_media_next_track(self):
      await self.api.set_command("next")

  async def async_media_previous_track(self):
      await self.api.set_command("prev")

  async def async_media_stop(self):
      await self.api.set_command("stop")
      self._state = MediaPlayerState.IDLE

  async def async_media_play(self):
      await self.api.set_command("start")
      self._state = MediaPlayerState.PLAYING

  async def async_media_pause(self):
      await self.api.set_command("stop")
      self._state = MediaPlayerState.IDLE
  
  async def async_turn_off(self):
      await self.api.set_command("turnoff")
      self._state = MediaPlayerState.OFF

  async def async_turn_on(self, **kwargs):
      await self.api.set_command("turnon")
      self._state = MediaPlayerState.ON

Zatem jeśli komponent jest napisany poprawnie to taka konfiguracja powinna dodać 2 mediaplayery

media_player:
# yoradio entity 1
  - platform: yoradio
    name: YoRadio Salon
    root_topic: salon/100
# yoradio entity 2
  - platform: yoradio
    name: YoRadio Kuchnia
    root_topic: kuchnia/100

Jeśli w ten sposób nie działa, to zgłoś issue u autora (no chyba że zajrzy tu jakiś spec od tworzenia integracji i to ogarnie, ale zwykle najlepiej zgłosić issue u autora, bo on już zna temat)

SUPER!! :+1:
dodało drugą encje mediaplayera jest tak jak chciałem próbowałem w ten sposób ale nie wstawiłem # yoradio entity 2 i występował błąd zdublowanej linii.
Dzięki za pomoc

To co jest za płotkiem (#) to tylko komentarz (te linie z płotkami można wyrzucić, dałem je tylko po to, aby wyróżnić co jest definicją każdego z odtwarzaczy).

Szkoda, że nie wstawiłeś tych złych konfiguracji, to bym pokazał gdzie był błąd.
Myślę, że miałeś zdublowane jakieś wpisy może media_player:?
(albo inne klucze - nawet nazwy nie mogą się pokrywać, jeśli encje nie mają unikalnych identyfikatorów)

Nie mam tego zainstalowanego więc nie mogłem sprawdzić czy się da dodać im unique_id więc możesz za-eksperymentować tak (to powinno dodać pewne opcje w GUI HA).

media_player:
  - platform: yoradio
    name: YoRadio Salon
    root_topic: salon/100
    unique_id: radyjko_salon1
  - platform: yoradio
    name: YoRadio Kuchnia
    root_topic: kuchnia/100
    unique_id: radyjko_kuchnia1

Komponent niestandardowy może nie obsługiwać unikalnych identyfikatorów (choć powinien, ale jak mówiłem nie mam na czym testować), a wtedy “sam sos” stanowi to

media_player: 
  - platform: yoradio
    name: YoRadio Salon
    root_topic: salon/100
  - platform: yoradio
    name: YoRadio Kuchnia
    root_topic: kuchnia/100

a tak z komentarzami

media_player: # to jest sekcja wszystkich media-playerów i w całym YAMLu może być tylko jedna 
  - platform: yoradio # tu definiujesz platformę danego odtwarzacza - to z jakiej integracji on pochodzi
    name: YoRadio Salon #nazwy encji muszą być różne jeśli nie mają unikalnych identyfikatorów
    root_topic: salon/100 #a to już wynika z twojego eksperymentu gdzie trzeba wprowadzić rozróżnienie na etapie MQTT
  - platform: yoradio
    name: YoRadio Kuchnia
    root_topic: kuchnia/100