Home Assistant Unofficial Reference 2024.12.1
server.py
Go to the documentation of this file.
1 """Code to handle the api connection to a Roon server."""
2 
3 import asyncio
4 import logging
5 
6 from roonapi import RoonApi, RoonDiscovery
7 
8 from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
9 from homeassistant.helpers.dispatcher import async_dispatcher_send
10 from homeassistant.util.dt import utcnow
11 
12 from .const import CONF_ROON_ID, ROON_APPINFO
13 
14 _LOGGER = logging.getLogger(__name__)
15 INITIAL_SYNC_INTERVAL = 5
16 FULL_SYNC_INTERVAL = 30
17 
18 
19 class RoonServer:
20  """Manages a single Roon Server."""
21 
22  def __init__(self, hass, config_entry):
23  """Initialize the system."""
24  self.config_entryconfig_entry = config_entry
25  self.hasshass = hass
26  self.roonapiroonapi = None
27  self.roon_idroon_id = None
28  self.all_player_idsall_player_ids = set()
29  self.all_playlistsall_playlists = []
30  self.offline_devicesoffline_devices = set()
31  self._exit_exit = False
32  self._roon_name_by_id_roon_name_by_id = {}
33  self._id_by_roon_name_id_by_roon_name = {}
34 
35  async def async_setup(self, tries=0):
36  """Set up a roon server based on config parameters."""
37 
38  def get_roon_host():
39  host = self.config_entryconfig_entry.data.get(CONF_HOST)
40  port = self.config_entryconfig_entry.data.get(CONF_PORT)
41  if host:
42  _LOGGER.debug("static roon core host=%s port=%s", host, port)
43  return (host, port)
44 
45  discover = RoonDiscovery(core_id)
46  server = discover.first()
47  discover.stop()
48  _LOGGER.debug("dynamic roon core core_id=%s server=%s", core_id, server)
49  return (server[0], server[1])
50 
51  def get_roon_api():
52  token = self.config_entryconfig_entry.data[CONF_API_KEY]
53  (host, port) = get_roon_host()
54  return RoonApi(ROON_APPINFO, token, host, port, blocking_init=True)
55 
56  core_id = self.config_entryconfig_entry.data.get(CONF_ROON_ID)
57 
58  self.roonapiroonapi = await self.hasshass.async_add_executor_job(get_roon_api)
59 
60  self.roonapiroonapi.register_state_callback(
61  self.roonapi_state_callbackroonapi_state_callback, event_filter=["zones_changed"]
62  )
63 
64  # Default to 'host' for compatibility with older configs without core_id
65  self.roon_idroon_id = (
66  core_id if core_id is not None else self.config_entryconfig_entry.data[CONF_HOST]
67  )
68 
69  # Initialize Roon background polling
70  self.config_entryconfig_entry.async_create_background_task(
71  self.hasshass, self.async_do_loopasync_do_loop(), "roon.server-do-loop"
72  )
73 
74  return True
75 
76  async def async_reset(self):
77  """Reset this connection to default state.
78 
79  Will cancel any scheduled setup retry and will unload
80  the config entry.
81  """
82  self.stop_roonstop_roon()
83  return True
84 
85  @property
86  def zones(self):
87  """Return list of zones."""
88  return self.roonapiroonapi.zones
89 
90  def add_player_id(self, entity_id, roon_name):
91  """Register a roon player."""
92  self._roon_name_by_id_roon_name_by_id[entity_id] = roon_name
93  self._id_by_roon_name_id_by_roon_name[roon_name] = entity_id
94 
95  def roon_name(self, entity_id):
96  """Get the name of the roon player from entity_id."""
97  return self._roon_name_by_id_roon_name_by_id.get(entity_id)
98 
99  def entity_id(self, roon_name):
100  """Get the id of the roon player from the roon name."""
101  return self._id_by_roon_name_id_by_roon_name.get(roon_name)
102 
103  def stop_roon(self):
104  """Stop background worker."""
105  self.roonapiroonapi.stop()
106  self._exit_exit = True
107 
108  def roonapi_state_callback(self, event, changed_zones):
109  """Callbacks from the roon api websocket with state change."""
110  self.hasshass.add_job(self.async_update_changed_playersasync_update_changed_players(changed_zones))
111 
112  async def async_do_loop(self):
113  """Background work loop."""
114  self._exit_exit = False
115  await asyncio.sleep(INITIAL_SYNC_INTERVAL)
116  while not self._exit_exit:
117  await self.async_update_playersasync_update_players()
118  await asyncio.sleep(FULL_SYNC_INTERVAL)
119 
120  async def async_update_changed_players(self, changed_zones_ids):
121  """Update the players which were reported as changed by the Roon API."""
122  _LOGGER.debug("async_update_changed_players %s", changed_zones_ids)
123  for zone_id in changed_zones_ids:
124  if zone_id not in self.roonapiroonapi.zones:
125  # device was removed ?
126  continue
127  zone = self.roonapiroonapi.zones[zone_id]
128  for device in zone["outputs"]:
129  dev_name = device["display_name"]
130  if dev_name == "Unnamed" or not dev_name:
131  # ignore unnamed devices
132  continue
133  player_data = await self.async_create_player_dataasync_create_player_data(zone, device)
134  dev_id = player_data["dev_id"]
135  player_data["is_available"] = True
136  if dev_id in self.offline_devicesoffline_devices:
137  # player back online
138  self.offline_devicesoffline_devices.remove(dev_id)
139  async_dispatcher_send(self.hasshass, "roon_media_player", player_data)
140  self.all_player_idsall_player_ids.add(dev_id)
141 
142  async def async_update_players(self):
143  """Periodic full scan of all devices."""
144  zone_ids = self.roonapiroonapi.zones.keys()
145  _LOGGER.debug("async_update_players %s", zone_ids)
146  await self.async_update_changed_playersasync_update_changed_players(zone_ids)
147  # check for any removed devices
148  all_devs = {}
149  for zone in self.roonapiroonapi.zones.values():
150  for device in zone["outputs"]:
151  player_data = await self.async_create_player_dataasync_create_player_data(zone, device)
152  dev_id = player_data["dev_id"]
153  all_devs[dev_id] = player_data
154  for dev_id in self.all_player_idsall_player_ids:
155  if dev_id in all_devs:
156  continue
157  # player was removed!
158  player_data = {"dev_id": dev_id}
159  player_data["is_available"] = False
160  async_dispatcher_send(self.hasshass, "roon_media_player", player_data)
161  self.offline_devicesoffline_devices.add(dev_id)
162 
163  async def async_create_player_data(self, zone, output):
164  """Create player object dict by combining zone with output."""
165  new_dict = zone.copy()
166  new_dict.update(output)
167  new_dict.pop("outputs")
168  new_dict["roon_id"] = self.roon_idroon_id
169  new_dict["is_synced"] = len(zone["outputs"]) > 1
170  new_dict["zone_name"] = zone["display_name"]
171  new_dict["display_name"] = output["display_name"]
172  new_dict["last_changed"] = utcnow()
173  # we don't use the zone_id or output_id for now as unique id as I've seen cases were it changes for some reason
174  new_dict["dev_id"] = f"roon_{self.roon_id}_{output['display_name']}"
175  return new_dict
def async_create_player_data(self, zone, output)
Definition: server.py:163
def roonapi_state_callback(self, event, changed_zones)
Definition: server.py:108
def async_update_changed_players(self, changed_zones_ids)
Definition: server.py:120
def __init__(self, hass, config_entry)
Definition: server.py:22
def add_player_id(self, entity_id, roon_name)
Definition: server.py:90
bool add(self, _T matcher)
Definition: match.py:185
bool remove(self, _T matcher)
Definition: match.py:214
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193