예제 #1
0
    def __init__(self, device_name, mqtt_connection, connection_callback):
        """
        Called if a new Chromecast device has been found.
        """

        self.logger = logging.getLogger("chromecast")
        self.device_name = device_name
        self.connection_callback = connection_callback
        self.connection_failure_count = 0
        self.device_connected = False

        self.mqtt_properties = MqttPropertyHandler(mqtt_connection, device_name, self)
        self.processing_queue = Queue(maxsize=100)

        self.processing_worker = Thread(target=self._worker)
        self.processing_worker.daemon = True
        self.processing_worker.start()

        self.processing_queue.put(CreateConnectionCommand(device_name))
예제 #2
0
class ChromecastConnection(MqttChangesCallback):
    def __init__(self, device_name, mqtt_connection, connection_callback):
        """
        Called if a new Chromecast device has been found.
        """

        self.logger = logging.getLogger("chromecast")
        self.device_name = device_name
        self.connection_callback = connection_callback
        self.connection_failure_count = 0
        self.device_connected = False

        self.mqtt_properties = MqttPropertyHandler(mqtt_connection,
                                                   device_name, self)
        self.processing_queue = Queue(maxsize=100)

        self.processing_worker = Thread(target=self._worker)
        self.processing_worker.daemon = True
        self.processing_worker.start()

        self.processing_queue.put(CreateConnectionCommand(device_name))

    def is_connected(self):
        # TODO thread sync
        return self.device_connected

    def unregister_device(self):
        """
        Called if this Chromecast device has disappeared and resources should be cleaned up.
        """

        self.processing_queue.put(DisconnectCommand())

    def is_interesting_message(self, topic):
        """
        Called to determine if the current device is interested in handling a MQTT topic. If true is
        returned, handle_message(topic, payload) is called next to handle the message.
        """
        return self.mqtt_properties.is_topic_filter_matching(topic)

    def handle_message(self, topic, payload):
        """
        Handle an incoming mqtt message.
        """

        self.mqtt_properties.handle_message(topic, payload)

    def new_cast_status(self, status):
        """
        PyChromecast cast status callback.
        """

        self.processing_queue.put(CastReceivedStatus(status))

    def new_launch_error(self, launch_failure):
        """
        PyChromecast error callback.
        """

        self.logger.error("received error from chromecast %s: %s" %
                          (self.device_name, launch_failure))

    def new_connection_status(self, status):
        """
        PyChromecast connection status callback.
        """

        self.processing_queue.put(CastConnectionStatus(status))

    def new_media_status(self, status):
        """
        PyChromecast media status callback.
        """

        self.processing_queue.put(CastMediaStatus(status))

    def on_volume_mute_requested(self, is_muted):
        self.processing_queue.put(VolumeMuteCommand(is_muted))

    def on_volume_level_relative_requested(self, relative_value):
        self.processing_queue.put(VolumeLevelRelativeCommand(relative_value))

    def on_volume_level_absolute_requested(self, absolute_value):
        self.processing_queue.put(VolumeLevelAbsoluteCommand(absolute_value))

    def on_player_position_requested(self, position):
        self.processing_queue.put(PlayerPositionCommand(position))

    def on_player_play_stream_requested(self, content_url, content_type):
        self.processing_queue.put(
            PlayerPlayStreamCommand(content_url, content_type))

    def on_player_pause_requested(self):
        self.processing_queue.put(PlayerPauseCommand())

    def on_player_resume_requested(self):
        self.processing_queue.put(PlayerResumeCommand())

    def on_player_stop_requested(self):
        self.processing_queue.put(PlayerStopCommand())

    def on_player_skip_requested(self):
        self.processing_queue.put(PlayerSkipCommand())

    def on_player_rewind_requested(self):
        self.processing_queue.put(PlayerRewindCommand())

    def _worker(self):
        while True:
            # TODO we should actually only get commands from the command queue if we are connected
            item = self.processing_queue.get()

            # noinspection PyBroadException
            try:
                requires_connection = not isinstance(item, CreateConnectionCommand) \
                                      and not isinstance(item, DisconnectCommand) \
                                      and not isinstance(item, CastReceivedStatus) \
                                      and not isinstance(item, CastConnectionStatus) \
                                      and not isinstance(item, CastMediaStatus)

                if requires_connection and not self.device_connected:
                    self.logger.info(
                        "no connection found but connection is required")
                    self._internal_create_connection(self.device_name)

                    if not self.device_connected:
                        self.logger.error(
                            "was not able to connect to device for command %s"
                            % (item, ))
                        raise ConnectionUnavailableException()

                if isinstance(item, CreateConnectionCommand):
                    self._worker_create_connection(item.device_name)
                elif isinstance(item, DisconnectCommand):
                    self._worker_disconnect()
                elif isinstance(item, VolumeMuteCommand):
                    self._worker_volume_muted(item.muted)
                elif isinstance(item, VolumeLevelRelativeCommand):
                    self._worker_volume_level_relative(item.value)
                elif isinstance(item, VolumeLevelAbsoluteCommand):
                    self._worker_volume_level_absolute(item.value)
                elif isinstance(item, PlayerPositionCommand):
                    self._worker_player_position(item.position)
                elif isinstance(item, PlayerPlayStreamCommand):
                    self._worker_player_play_stream(item.content_url,
                                                    item.content_type)
                elif isinstance(item, PlayerPauseCommand):
                    self._worker_player_pause()
                elif isinstance(item, PlayerResumeCommand):
                    self._worker_player_resume()
                elif isinstance(item, PlayerStopCommand):
                    self._worker_player_stop()
                elif isinstance(item, PlayerSkipCommand):
                    self._worker_player_skip()
                elif isinstance(item, PlayerRewindCommand):
                    self._worker_player_rewind()
                elif isinstance(item, CastReceivedStatus):
                    self._worker_cast_received_status(item.status)
                elif isinstance(item, CastConnectionStatus):
                    self._worker_cast_connection_status(item.status)
                elif isinstance(item, CastMediaStatus):
                    self._worker_cast_media_status(item.status)
            except Exception as error:
                self.logger.exception("command %s failed" % (item, ))

                if isinstance(error, ConnectionUnavailableException):
                    self.mqtt_properties.write_connection_status(
                        CONNECTION_STATUS_NOT_FOUND)
                else:
                    self.mqtt_properties.write_connection_status(
                        CONNECTION_STATUS_ERROR)

                # e.g. AttributeError: 'NoneType' object has no attribute 'media_controller'
                # at least something indicating that the connection is really dead for sure
                if isinstance(error, AttributeError):
                    self.connection_callback.on_connection_dead(
                        self, self.device_name)
                else:
                    self.connection_callback.on_connection_failed(
                        self, self.device_name)
            finally:
                self.logger.debug("command %s finished" % (item, ))
                self.processing_queue.task_done()

    def _internal_create_connection(self, device_name):
        try:
            self.mqtt_properties.write_connection_status(
                CONNECTION_STATUS_WAITING_FOR_DEVICE)
            devices = get_chromecasts(
                tries=5)  # TODO not the best way to do this, change with #3

            for device in devices:
                if device.device.friendly_name == device_name:
                    self.device = device
                    break

            if self.device is None:
                self.logger.error("was not able to find chromecast %s" %
                                  self.device_name)
                raise ConnectionUnavailableException()

            self.device.register_status_listener(self)
            self.device.media_controller.register_status_listener(self)
            self.device.register_launch_error_listener(self)
            self.device.register_connection_listener(self)

            self.device_connected = True  # alibi action
        except ChromecastConnectionError:
            self.logger.exception(
                "had connection error while finding chromecast %s" %
                self.device_name)

            self.device_connected = False

    def _worker_create_connection(self, device_name):
        # uncaught exceptions bubble to the try-except handler of the worker thread
        self._internal_create_connection(device_name)

        if not self.device_connected:
            self.mqtt_properties.write_connection_status(
                CONNECTION_STATUS_ERROR)

    def _worker_disconnect(self):
        self.logger.info("disconnecting chromecast %s" % self.device_name)

        self.device_connected = False

        if self.device is not None:
            self.device.disconnect()
            self.device = None
        else:
            self.logger.warning("device is not available (at disconnection)")

        self.mqtt_properties.write_connection_status(
            CONNECTION_STATUS_DISCONNECTED)

    def _worker_volume_muted(self, is_muted):
        self.logger.info("volume mute request, is muted = %s" % is_muted)

        self.device.set_volume_muted(is_muted)

    def _worker_volume_level_relative(self, relative_value):
        self.logger.info("volume change relative request, value = %d" %
                         relative_value)

        new_level = self.device.status.volume_level + (relative_value / 100)
        if new_level > 100:
            self.logger.warning(
                "received relative volume level that was too high")
            new_level = 100
        elif new_level < 0:
            self.logger.warning(
                "received relative volume level that was too low")
            new_level = 0

        self.device.set_volume(new_level)

    def _worker_volume_level_absolute(self, absolute_value):
        self.logger.info("volume change absolute request, value = %d" %
                         absolute_value)

        new_level = absolute_value / 100
        if new_level > 100:
            self.logger.warning(
                "received absolute volume level that was too high")
            new_level = 100
        elif new_level < 0:
            self.logger.warning(
                "received absolute volume level that was too low")
            new_level = 0

        self.device.set_volume(new_level)

    def _worker_player_position(self, position):
        self.logger.info("volume change position request, position = %d" %
                         position)

        self.device.media_controller.seek(position)

    def _worker_player_play_stream(self, content_url, content_type):
        self.logger.info("play stream request, url = %s, type = %s" %
                         (content_url, content_type))

        self.device.media_controller.play_media(content_url,
                                                content_type,
                                                autoplay=True)

    def _worker_player_pause(self):
        self.logger.info("pause request")

        self.device.media_controller.pause()

    def _worker_player_resume(self):
        self.logger.info("resume request")

        self.device.media_controller.play()

    def _worker_player_stop(self):
        self.logger.info("stop request")

        self.device.media_controller.stop()

    def _worker_player_skip(self):
        self.logger.info("skip request")

        self.device.media_controller.seek(
            int(self.device.media_controller.status.duration) - 1)

    def _worker_player_rewind(self):
        self.logger.info("rewind request")

        self.device.media_controller.rewind()

    # ##################################################################################

    def _worker_cast_received_status(self, status):
        # CastStatus(is_active_input=None, is_stand_by=None, volume_level=0.3499999940395355, volume_muted=False,
        # app_id='CC1AD845', display_name='Default Media Receiver', namespaces=['urn:x-cast:com.google.cast.media'],
        # session_id='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx', transport_id='web-0', status_text='Now Casting')
        self.logger.info("received new cast status from chromecast %s" %
                         self.device_name)

        if status is None:
            self.logger.warning("received empty status")
            return

        self.mqtt_properties.write_cast_status(status.display_name,
                                               status.volume_level,
                                               status.volume_muted)
        # dummy write as connection status callback does not work at the moment
        self.mqtt_properties.write_connection_status(
            CONNECTION_STATUS_CONNECTED)
        self.connection_failure_count = 0

        # reset player state if necessary
        if status.app_id is None or status.app_id == IDLE_APP_ID:  # no app active = idle
            self.mqtt_properties.write_player_status(MEDIA_PLAYER_STATE_IDLE,
                                                     None, None)

    def _worker_cast_connection_status(self, status):
        self.logger.info(
            "received new connection status from chromecast %s: %s" %
            (self.device_name, status.status))
        self.mqtt_properties.write_connection_status(status.status)

        self.device_connected = status.status == CONNECTION_STATUS_CONNECTED

        if status.status == CONNECTION_STATUS_CONNECTED:
            self.connection_failure_count = 0

            self.mqtt_properties.write_cast_data(self.device.cast_type,
                                                 self.device.name)
        elif status.status == CONNECTION_STATUS_FAILED:
            self.connection_failure_count += 1
            self.logger.warning(
                "received failure from connection, current failure counter: %d"
                % self.connection_failure_count)

            if self.connection_failure_count > 7:
                self.logger.warning(
                    "failure counter too high, treating chromecast as dead")
                self.connection_callback.on_connection_dead(
                    self, self.device_name)

    def _worker_cast_media_status(self, status):
        #  <MediaStatus {'media_metadata': {}, 'content_id': 'http://some.url.com/', 'player_state': 'PLAYING',
        # 'episode': None, 'media_custom_data': {}, 'supports_stream_mute': True, 'track': None,
        # 'supports_stream_volume': True, 'volume_level': 1, 'album_name': None, 'idle_reason': None,
        # 'album_artist': None, 'media_session_id': 4, 'content_type': 'audio/mpeg', 'metadata_type': None,
        # 'volume_muted': False, 'supports_pause': True, 'artist': None, 'title': None, 'subtitle_tracks': {},
        # 'supports_skip_backward': False, 'stream_type': 'BUFFERED', 'playback_rate': 1,
        # 'supports_skip_forward': False, 'season': None, 'duration': None, 'images': [], 'series_title': None,
        # 'supports_seek': True, 'current_time': 13938.854693, 'supported_media_commands': 15}>
        self.logger.info("received new media status from chromecast %s" %
                         self.device_name)

        images = status.media_metadata.get('images', [])
        image_filtered = None

        for image in images:
            image_filtered = image["url"]
            break  # only take the first image

        self.mqtt_properties.write_player_status(status.player_state,
                                                 status.current_time,
                                                 status.duration)
        self.mqtt_properties.write_media_status(
            status.title, status.album_name, status.artist,
            status.album_artist, status.track, image_filtered,
            status.content_type, status.content_id)