diff --git a/custom_components/dispatcharr_sensor/const.py b/custom_components/dispatcharr_sensor/const.py index 937f8a6..e0b935b 100644 --- a/custom_components/dispatcharr_sensor/const.py +++ b/custom_components/dispatcharr_sensor/const.py @@ -1,6 +1,5 @@ """Constants for the Dispatcharr Sensor integration.""" DOMAIN = "dispatcharr_sensor" - -# List of platforms to support. In this case, just the "sensor" platform. -PLATFORMS = ["sensor"] \ No newline at end of file +# Add BUTTON and MEDIA_PLAYER explicitly +PLATFORMS = ["sensor", "media_player", "button"] diff --git a/custom_components/dispatcharr_sensor/media_player.py b/custom_components/dispatcharr_sensor/media_player.py index 16f48a7..c442ec8 100644 --- a/custom_components/dispatcharr_sensor/media_player.py +++ b/custom_components/dispatcharr_sensor/media_player.py @@ -8,15 +8,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerDeviceClass, + MediaPlayerEntityFeature, MediaType, ) -from homeassistant.const import STATE_PLAYING +from homeassistant.const import STATE_PLAYING, STATE_IDLE from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.exceptions import PlatformNotReady from .const import DOMAIN -from . import DispatcharrDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -25,18 +24,13 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the media_player platform from a ConfigEntry.""" - try: - coordinator = hass.data[DOMAIN][config_entry.entry_id] - except KeyError: - raise PlatformNotReady(f"Coordinator not found for entry {config_entry.entry_id}") - + """Set up the media_player platform.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] DispatcharrStreamManager(coordinator, async_add_entities) - class DispatcharrStreamManager: """Manages the creation and removal of media_player entities.""" - def __init__(self, coordinator: DispatcharrDataUpdateCoordinator, async_add_entities: AddEntitiesCallback): + def __init__(self, coordinator, async_add_entities): self._coordinator = coordinator self._async_add_entities = async_add_entities self._known_stream_ids = set() @@ -44,7 +38,6 @@ class DispatcharrStreamManager: @callback def _update_entities(self) -> None: - """Update, add, or remove entities based on coordinator data.""" if self._coordinator.data is None: current_stream_ids = set() else: @@ -57,13 +50,14 @@ class DispatcharrStreamManager: self._known_stream_ids.update(new_stream_ids) class DispatcharrStreamMediaPlayer(CoordinatorEntity, MediaPlayerEntity): - """Representation of a single Dispatcharr stream as a Media Player.""" + """Dispatcharr stream player.""" _attr_should_poll = False _attr_has_entity_name = True _attr_device_class = MediaPlayerDeviceClass.TV - _attr_supported_features = 0 # Read-only entity supports no features + # Feature: Allow stopping the stream + _attr_supported_features = MediaPlayerEntityFeature.STOP - def __init__(self, coordinator: DispatcharrDataUpdateCoordinator, stream_id: str): + def __init__(self, coordinator, stream_id: str): super().__init__(coordinator) self._stream_id = stream_id @@ -76,26 +70,34 @@ class DispatcharrStreamMediaPlayer(CoordinatorEntity, MediaPlayerEntity): @property def available(self) -> bool: - """Return True if the stream is still in the coordinator's data.""" return super().available and self.coordinator.data is not None and self._stream_id in self.coordinator.data - # ADDED: This property override directly prevents the TypeError. - @property - def support_grouping(self) -> bool: - """Flag if grouping is supported.""" - return False + async def async_media_stop(self) -> None: + """Send a command to Dispatcharr to kill this stream.""" + _LOGGER.info("Stopping stream %s via API", self._stream_id) + try: + # Uses the DELETE endpoint defined in Swagger + await self.coordinator.api_request("DELETE", f"/proxy/ts/stop/{self._stream_id}") + # Optimistic state update + self._attr_state = STATE_IDLE + self.async_write_ha_state() + # Force refresh to clear the entity + await self.coordinator.async_request_refresh() + except Exception as e: + _LOGGER.error("Failed to stop stream: %s", e) @callback def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" if not self.available: + self._attr_state = STATE_IDLE self.async_write_ha_state() + # If stream is gone, the Manager will eventually handle cleanup, + # but for now we just show idle. return stream_data = self.coordinator.data[self._stream_id] program_data = stream_data.get("program") or {} - # Set standard media player properties self._attr_state = STATE_PLAYING self._attr_app_name = "Dispatcharr" self._attr_entity_picture = stream_data.get("logo_url") @@ -103,23 +105,11 @@ class DispatcharrStreamMediaPlayer(CoordinatorEntity, MediaPlayerEntity): self._attr_media_series_title = program_data.get("title") self._attr_media_title = program_data.get("subtitle") or program_data.get("title") - # Parse season and episode number - self._attr_media_season = None - self._attr_media_episode = None - episode_num_str = program_data.get("episode_num") - if episode_num_str: - match = re.search(r'S(\d+)E(\d+)', episode_num_str, re.IGNORECASE) - if match: - self._attr_media_season = int(match.group(1)) - self._attr_media_episode = int(match.group(2)) - - # Store other details in extra attributes + # Extra attributes self._attr_extra_state_attributes = { "channel_number": stream_data.get("xmltv_id"), "channel_name": stream_data.get("channel_name"), "program_description": program_data.get("description"), - "program_start": program_data.get("start_time"), - "program_stop": program_data.get("end_time"), "clients": stream_data.get("client_count"), "resolution": stream_data.get("resolution"), "video_codec": stream_data.get("video_codec"),