1 """Platform for Control4 Rooms Media Players."""
3 from __future__
import annotations
5 from dataclasses
import dataclass
6 from datetime
import timedelta
11 from pyControl4.error_handling
import C4Exception
12 from pyControl4.room
import C4Room
15 MediaPlayerDeviceClass,
17 MediaPlayerEntityFeature,
27 from .const
import CONF_DIRECTOR, CONF_DIRECTOR_ALL_ITEMS, CONF_UI_CONFIGURATION, DOMAIN
28 from .director_utils
import update_variables_for_config_entry
29 from .entity
import Control4Entity
31 _LOGGER = logging.getLogger(__name__)
33 CONTROL4_POWER_STATE =
"POWER_STATE"
34 CONTROL4_VOLUME_STATE =
"CURRENT_VOLUME"
35 CONTROL4_MUTED_STATE =
"IS_MUTED"
36 CONTROL4_CURRENT_VIDEO_DEVICE =
"CURRENT_VIDEO_DEVICE"
37 CONTROL4_PLAYING =
"PLAYING"
38 CONTROL4_PAUSED =
"PAUSED"
39 CONTROL4_STOPPED =
"STOPPED"
40 CONTROL4_MEDIA_INFO =
"CURRENT MEDIA INFO"
42 CONTROL4_PARENT_ID =
"parentId"
44 VARIABLES_OF_INTEREST = {
46 CONTROL4_VOLUME_STATE,
48 CONTROL4_CURRENT_VIDEO_DEVICE,
63 """Class for Room Source."""
65 source_type: set[_SourceType]
70 async
def get_rooms(hass: HomeAssistant, entry: ConfigEntry):
71 """Return a list of all Control4 rooms."""
72 director_all_items = hass.data[DOMAIN][entry.entry_id][CONF_DIRECTOR_ALL_ITEMS]
75 for item
in director_all_items
76 if "typeName" in item
and item[
"typeName"] ==
"room"
81 hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
83 """Set up Control4 rooms from a config entry."""
84 entry_data = hass.data[DOMAIN][entry.entry_id]
85 ui_config = entry_data[CONF_UI_CONFIGURATION]
89 _LOGGER.debug(
"No UI Configuration found for Control4")
96 scan_interval = entry_data[CONF_SCAN_INTERVAL]
97 _LOGGER.debug(
"Scan interval = %s", scan_interval)
99 async
def async_update_data() -> dict[int, dict[str, Any]]:
100 """Fetch data from Control4 director."""
103 hass, entry, VARIABLES_OF_INTEREST
105 except C4Exception
as err:
106 raise UpdateFailed(f
"Error communicating with API: {err}")
from err
108 coordinator = DataUpdateCoordinator[dict[int, dict[str, Any]]](
112 update_method=async_update_data,
113 update_interval=
timedelta(seconds=scan_interval),
117 await coordinator.async_refresh()
121 for item
in hass.data[DOMAIN][entry.entry_id][CONF_DIRECTOR_ALL_ITEMS]
123 item_to_parent_map = {
125 for k, item
in items_by_id.items()
126 if "parentId" in item
and k > 1
130 for room
in all_rooms:
133 sources: dict[int, _RoomSource] = {}
134 for exp
in ui_config[
"experiences"]:
135 if room_id == exp[
"room_id"]:
136 exp_type = exp[
"type"]
137 if exp_type
not in (
"listen",
"watch"):
141 _SourceType.AUDIO
if exp_type ==
"listen" else _SourceType.VIDEO
143 for source
in exp[
"sources"][
"source"]:
144 dev_id = source[
"id"]
145 name = items_by_id.get(dev_id, {}).
get(
146 "name", f
"Unknown Device - {dev_id}"
148 if dev_id
in sources:
149 sources[dev_id].source_type.add(dev_type)
152 source_type={dev_type}, idx=dev_id, name=name
156 hidden = room[
"roomHidden"]
170 "Unknown device properties received from Control4: %s",
179 """Control4 Room entity."""
181 _attr_has_entity_name =
True
186 coordinator: DataUpdateCoordinator[dict[int, dict[str, Any]]],
189 id_to_parent: dict[int, int],
190 sources: dict[int, _RoomSource],
193 """Initialize Control4 room entity."""
200 device_manufacturer=
None,
208 MediaPlayerEntityFeature.PLAY
209 | MediaPlayerEntityFeature.PAUSE
210 | MediaPlayerEntityFeature.STOP
211 | MediaPlayerEntityFeature.VOLUME_MUTE
212 | MediaPlayerEntityFeature.VOLUME_SET
213 | MediaPlayerEntityFeature.VOLUME_STEP
214 | MediaPlayerEntityFeature.TURN_OFF
215 | MediaPlayerEntityFeature.SELECT_SOURCE
219 """Create a pyControl4 device object.
221 This exists so the director token used is always the latest one, without needing to re-init the entire entity.
223 return C4Room(self.
entry_dataentry_data[CONF_DIRECTOR], self.
_idx_idx)
226 current_device = self.coordinator.data[self.
_idx_idx][var]
227 if current_device == 0:
230 return current_device
238 if "medSrcDev" in media_info:
239 return media_info[
"medSrcDev"]
240 if "deviceid" in media_info:
241 return media_info[
"deviceid"]
245 """Get the Media Info Dictionary if populated."""
246 media_info = self.coordinator.data[self.
_idx_idx][CONTROL4_MEDIA_INFO]
247 if "mediainfo" in media_info:
248 return media_info[
"mediainfo"]
253 while current_source:
254 current_data = self.coordinator.data.get(current_source,
None)
256 if current_data.get(CONTROL4_PLAYING,
None):
257 return MediaPlayerState.PLAYING
258 if current_data.get(CONTROL4_PAUSED,
None):
259 return MediaPlayerState.PAUSED
260 if current_data.get(CONTROL4_STOPPED,
None):
261 return MediaPlayerState.ON
267 """Return the class of this entity."""
268 for avail_source
in self.
_sources_sources.values():
269 if _SourceType.VIDEO
in avail_source.source_type:
270 return MediaPlayerDeviceClass.TV
271 return MediaPlayerDeviceClass.SPEAKER
275 """Return whether this room is on or idle."""
280 if self.coordinator.data[self.
_idx_idx][CONTROL4_POWER_STATE]:
281 return MediaPlayerState.ON
283 return MediaPlayerState.IDLE
287 """Get the current source."""
289 if not current_source
or current_source
not in self.
_sources_sources:
291 return self.
_sources_sources[current_source].name
295 """Get the Media Title."""
299 if "title" in media_info:
300 return media_info[
"title"]
302 if not current_source
or current_source
not in self.
_sources_sources:
304 return self.
_sources_sources[current_source].name
308 """Get current content type if available."""
310 if not current_source:
313 return MediaType.VIDEO
314 return MediaType.MUSIC
317 """If possible, toggle the current play/pause state.
319 Not every source supports play/pause.
320 Unfortunately MediaPlayer capabilities are not dynamic,
321 so we must determine if play/pause is supported here
328 """Get the available source."""
329 return [x.name
for x
in self.
_sources_sources.values()]
333 """Get the volume level."""
334 return self.coordinator.data[self.
_idx_idx][CONTROL4_VOLUME_STATE] / 100
338 """Check if the volume is muted."""
339 return bool(self.coordinator.data[self.
_idx_idx][CONTROL4_MUTED_STATE])
342 """Select a new source."""
343 for avail_source
in self.
_sources_sources.values():
344 if avail_source.name == source:
345 audio_only = _SourceType.VIDEO
not in avail_source.source_type
354 await self.coordinator.async_request_refresh()
357 """Turn off the room."""
359 await self.coordinator.async_request_refresh()
367 await self.coordinator.async_request_refresh()
370 """Set room volume, 0-1 scale."""
372 await self.coordinator.async_request_refresh()
375 """Increase the volume by 1."""
377 await self.coordinator.async_request_refresh()
380 """Decrease the volume by 1."""
382 await self.coordinator.async_request_refresh()
385 """Issue a pause command."""
387 await self.coordinator.async_request_refresh()
390 """Issue a play command."""
392 await self.coordinator.async_request_refresh()
395 """Issue a stop command."""
397 await self.coordinator.async_request_refresh()
web.Response get(self, web.Request request, str config_key)
dict[int, dict[str, Any]] update_variables_for_config_entry(HomeAssistant hass, ConfigEntry entry, set[str] variable_names)