pax_global_header00006660000000000000000000000064135517713220014520gustar00rootroot0000000000000052 comment=99362ae58ac65f747f8986db8eaa7120e4db3c8d pychromecast-4.1.0/000077500000000000000000000000001355177132200142235ustar00rootroot00000000000000pychromecast-4.1.0/.github/000077500000000000000000000000001355177132200155635ustar00rootroot00000000000000pychromecast-4.1.0/.github/release-drafter.yml000066400000000000000000000000541355177132200213520ustar00rootroot00000000000000template: | ## What's Changed $CHANGES pychromecast-4.1.0/.gitignore000066400000000000000000000007741355177132200162230ustar00rootroot00000000000000# Hide sublime text stuff *.sublime-project *.sublime-workspace # Hide some OS X stuff .DS_Store .AppleDouble .LSOverride Icon # Thumbnails ._* # GITHUB Proposed Python stuff: *.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Build files README.rst # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevprojectpychromecast-4.1.0/.travis.yml000066400000000000000000000004721355177132200163370ustar00rootroot00000000000000sudo: false language: python python: - "3.6" - "3.7" install: - pip install -r requirements.txt - pip install flake8==3.7.8 pylint==2.3.1 black==19.3b0 script: - flake8 --exclude cast_channel_pb2.py,authority_keys_pb2.py,logging_pb2.py pychromecast - pylint pychromecast - black pychromecast --check pychromecast-4.1.0/LICENSE000066400000000000000000000020731355177132200152320ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2013 Paulus Schoutsen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pychromecast-4.1.0/MANIFEST.in000066400000000000000000000001541355177132200157610ustar00rootroot00000000000000include README.rst include LICENSE include requirements.txt graft pychromecast recursive-exclude * *.py[co] pychromecast-4.1.0/README.rst000066400000000000000000000152271355177132200157210ustar00rootroot00000000000000pychromecast |Build Status| =========================== .. |Build Status| image:: https://travis-ci.org/balloob/pychromecast.svg?branch=master :target: https://travis-ci.org/balloob/pychromecast Library for Python 3.6+ to communicate with the Google Chromecast. It currently supports: - Auto discovering connected Chromecasts on the network - Start the default media receiver and play any online media - Control playback of current playing media - Implement Google Chromecast api v2 - Communicate with apps via channels - Easily extendable to add support for unsupported namespaces - Multi-room setups with Audio cast devices *Check out* `Home Assistant `_ *for a ready-made solution using PyChromecast for controlling and automating your Chromecast or Cast-enabled device like Google Home.* Dependencies ------------ PyChromecast depends on the Python packages requests, protobuf and zeroconf. Make sure you have these dependencies installed using ``pip install -r requirements.txt`` How to use ---------- .. code:: python >> import time >> import pychromecast >> chromecasts = pychromecast.get_chromecasts() >> [cc.device.friendly_name for cc in chromecasts] ['Dev', 'Living Room', 'Den', 'Bedroom'] >> cast = next(cc for cc in chromecasts if cc.device.friendly_name == "Living Room") >> # Start worker thread and wait for cast device to be ready >> cast.wait() >> print(cast.device) DeviceStatus(friendly_name='Living Room', model_name='Chromecast', manufacturer='Google Inc.', uuid=UUID('df6944da-f016-4cb8-97d0-3da2ccaa380b'), cast_type='cast') >> print(cast.status) CastStatus(is_active_input=True, is_stand_by=False, volume_level=1.0, volume_muted=False, app_id='CC1AD845', display_name='Default Media Receiver', namespaces=['urn:x-cast:com.google.cast.player.message', 'urn:x-cast:com.google.cast.media'], session_id='CCA39713-9A4F-34A6-A8BF-5D97BE7ECA5C', transport_id='web-9', status_text='') >> mc = cast.media_controller >> mc.play_media('http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', 'video/mp4') >> mc.block_until_active() >> print(mc.status) MediaStatus(current_time=42.458322, content_id='http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', content_type='video/mp4', duration=596.474195, stream_type='BUFFERED', idle_reason=None, media_session_id=1, playback_rate=1, player_state='PLAYING', supported_media_commands=15, volume_level=1, volume_muted=False) >> mc.pause() >> time.sleep(5) >> mc.play() Adding support for extra namespaces ----------------------------------- Each app that runs on the Chromecast supports namespaces. They specify a JSON-based mini-protocol. This is used to communicate between the Chromecast and your phone/browser and now Python. Support for extra namespaces is added by using controllers. To add your own namespace to a current chromecast instance you will first have to define your controller. Example of a minimal controller: .. code:: python from pychromecast.controllers import BaseController class MyController(BaseController): def __init__(self): super(MyController, self).__init__( "urn:x-cast:my.super.awesome.namespace") def receive_message(self, message, data): print("Wow, I received this message: {}".format(data)) return True # indicate you handled this message def request_beer(self): self.send_message({'request': 'beer'}) After you have defined your controller you will have to add an instance to a Chromecast object: `cast.register_handler(MyController())`. When a message is received with your namespace it will be routed to your controller. For more options see the `BaseController`_. For an example of a fully implemented controller see the `MediaController`_. .. _BaseController: https://github.com/balloob/pychromecast/blob/master/pychromecast/controllers/__init__.py .. _MediaController: https://github.com/balloob/pychromecast/blob/master/pychromecast/controllers/media.py Exploring existing namespaces ------------------------------- So you've got PyChromecast running and decided it is time to add support to your favorite app. No worries, the following instructions will have you covered in exploring the possibilities. The following instructions require the use of the `Google Chrome browser`_ and the `Google Cast plugin`_. * In Chrome, go to `chrome://net-internals/#capture` * Enable the checkbox 'Include the actual bytes sent/received.' * Open a new tab, browse to your favorite application on the web that has Chromecast support and start casting. * Go back to the tab that is capturing events and click on stop. * From the dropdown click on events. This will show you a table with events that happened while you were recording. * In the filter box enter the text `Tr@n$p0rt`. This should give one SOCKET connection as result: the connection with your Chromecast. * Go through the results and collect the JSON that is exchanged. * Now write a controller that is able to mimic this behavior :-) .. _Google Chrome Browser: https://www.google.com/chrome/ .. _Google Cast Plugin: https://chrome.google.com/webstore/detail/google-cast/boadgeojelhgndaghljhdicfkmllpafd Ignoring CEC Data ----------------- The Chromecast typically reports whether it is the active input on the device to which it is connected. This value is stored inside a cast object in the following property. .. code:: python cast.status.is_active_input Some Chromecast users have reported CEC incompatibilities with their media center devices. These incompatibilities may sometimes cause this active input value to be reported improperly. This active input value is typically used to determine if the Chromecast is idle. PyChromecast is capable of ignoring the active input value when determining if the Chromecast is idle in the instance that the Chromecast is returning erroneous values. To ignore this CEC detection data in PyChromecast, append a `Linux style wildcard`_ formatted string to the IGNORE\_CEC list in PyChromecast like in the example below. .. code:: python pychromecast.IGNORE_CEC.append('*') # Ignore CEC on all devices pychromecast.IGNORE_CEC.append('Living Room') # Ignore CEC on Chromecasts named Living Room Thanks ------ I would like to thank `Fred Clift`_ for laying the socket client ground work. Without him it would not have been possible! .. _Linux style wildcard: http://tldp.org/LDP/GNU-Linux-Tools-Summary/html/x11655.htm .. _@am0s: https://github.com/am0s .. _@rmkraus: https://github.com/rmkraus .. _@balloob: https://github.com/balloob .. _Fred Clift: https://github.com/minektur pychromecast-4.1.0/__init__.py000066400000000000000000000000001355177132200163220ustar00rootroot00000000000000pychromecast-4.1.0/chromecast_protobuf/000077500000000000000000000000001355177132200202735ustar00rootroot00000000000000pychromecast-4.1.0/chromecast_protobuf/README.md000066400000000000000000000002351355177132200215520ustar00rootroot00000000000000These files were imported from https://chromium.googlesource.com/chromium/src.git/+/master/extensions/common/api/cast_channel to generate the \_pb2.py-files.pychromecast-4.1.0/chromecast_protobuf/authority_keys.proto000066400000000000000000000006311355177132200244430ustar00rootroot00000000000000// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. syntax = "proto2"; option optimize_for = LITE_RUNTIME; package extensions.api.cast_channel.proto; message AuthorityKeys { message Key { required bytes fingerprint = 1; required bytes public_key = 2; } repeated Key keys = 1; } pychromecast-4.1.0/chromecast_protobuf/cast_channel.proto000066400000000000000000000057001355177132200240040ustar00rootroot00000000000000// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. syntax = "proto2"; option optimize_for = LITE_RUNTIME; package extensions.api.cast_channel; message CastMessage { // Always pass a version of the protocol for future compatibility // requirements. enum ProtocolVersion { CASTV2_1_0 = 0; } required ProtocolVersion protocol_version = 1; // source and destination ids identify the origin and destination of the // message. They are used to route messages between endpoints that share a // device-to-device channel. // // For messages between applications: // - The sender application id is a unique identifier generated on behalf of // the sender application. // - The receiver id is always the the session id for the application. // // For messages to or from the sender or receiver platform, the special ids // 'sender-0' and 'receiver-0' can be used. // // For messages intended for all endpoints using a given channel, the // wildcard destination_id '*' can be used. required string source_id = 2; required string destination_id = 3; // This is the core multiplexing key. All messages are sent on a namespace // and endpoints sharing a channel listen on one or more namespaces. The // namespace defines the protocol and semantics of the message. required string namespace = 4; // Encoding and payload info follows. // What type of data do we have in this message. enum PayloadType { STRING = 0; BINARY = 1; } required PayloadType payload_type = 5; // Depending on payload_type, exactly one of the following optional fields // will always be set. optional string payload_utf8 = 6; optional bytes payload_binary = 7; } enum SignatureAlgorithm { UNSPECIFIED = 0; RSASSA_PKCS1v15 = 1; RSASSA_PSS = 2; } enum HashAlgorithm { SHA1 = 0; SHA256 = 1; } // Messages for authentication protocol between a sender and a receiver. message AuthChallenge { optional SignatureAlgorithm signature_algorithm = 1 [default = RSASSA_PKCS1v15]; optional bytes sender_nonce = 2; optional HashAlgorithm hash_algorithm = 3 [default = SHA1]; } message AuthResponse { required bytes signature = 1; required bytes client_auth_certificate = 2; repeated bytes intermediate_certificate = 3; optional SignatureAlgorithm signature_algorithm = 4 [default = RSASSA_PKCS1v15]; optional bytes sender_nonce = 5; optional HashAlgorithm hash_algorithm = 6 [default = SHA1]; optional bytes crl = 7; } message AuthError { enum ErrorType { INTERNAL_ERROR = 0; NO_TLS = 1; // The underlying connection is not TLS SIGNATURE_ALGORITHM_UNAVAILABLE = 2; } required ErrorType error_type = 1; } message DeviceAuthMessage { // Request fields optional AuthChallenge challenge = 1; // Response fields optional AuthResponse response = 2; optional AuthError error = 3; } pychromecast-4.1.0/chromecast_protobuf/logging.proto000066400000000000000000000120521355177132200230060ustar00rootroot00000000000000// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. syntax = "proto2"; option optimize_for = LITE_RUNTIME; package extensions.api.cast_channel.proto; enum EventType { EVENT_TYPE_UNKNOWN = 0; CAST_SOCKET_CREATED = 1; READY_STATE_CHANGED = 2; CONNECTION_STATE_CHANGED = 3; READ_STATE_CHANGED = 4; WRITE_STATE_CHANGED = 5; ERROR_STATE_CHANGED = 6; CONNECT_FAILED = 7; TCP_SOCKET_CONNECT = 8; // Logged with RV. TCP_SOCKET_SET_KEEP_ALIVE = 9; SSL_CERT_WHITELISTED = 10; SSL_SOCKET_CONNECT = 11; // Logged with RV. SSL_INFO_OBTAINED = 12; DER_ENCODED_CERT_OBTAIN = 13; // Logged with RV. RECEIVED_CHALLENGE_REPLY = 14; AUTH_CHALLENGE_REPLY = 15; CONNECT_TIMED_OUT = 16; SEND_MESSAGE_FAILED = 17; MESSAGE_ENQUEUED = 18; // Message SOCKET_WRITE = 19; // Logged with RV. MESSAGE_WRITTEN = 20; // Message SOCKET_READ = 21; // Logged with RV. MESSAGE_READ = 22; // Message SOCKET_CLOSED = 25; SSL_CERT_EXCESSIVE_LIFETIME = 26; CHANNEL_POLICY_ENFORCED = 27; TCP_SOCKET_CONNECT_COMPLETE = 28; // Logged with RV. SSL_SOCKET_CONNECT_COMPLETE = 29; // Logged with RV. SSL_SOCKET_CONNECT_FAILED = 30; // Logged with RV. SEND_AUTH_CHALLENGE_FAILED = 31; // Logged with RV. AUTH_CHALLENGE_REPLY_INVALID = 32; PING_WRITE_ERROR = 33; // Logged with RV. } enum ChannelAuth { // SSL over TCP. SSL = 1; // SSL over TCP with challenge and receiver signature verification. SSL_VERIFIED = 2; } enum ReadyState { READY_STATE_NONE = 1; READY_STATE_CONNECTING = 2; READY_STATE_OPEN = 3; READY_STATE_CLOSING = 4; READY_STATE_CLOSED = 5; } enum ConnectionState { CONN_STATE_UNKNOWN = 1; CONN_STATE_TCP_CONNECT = 2; CONN_STATE_TCP_CONNECT_COMPLETE = 3; CONN_STATE_SSL_CONNECT = 4; CONN_STATE_SSL_CONNECT_COMPLETE = 5; CONN_STATE_AUTH_CHALLENGE_SEND = 6; CONN_STATE_AUTH_CHALLENGE_SEND_COMPLETE = 7; CONN_STATE_AUTH_CHALLENGE_REPLY_COMPLETE = 8; CONN_STATE_START_CONNECT = 9; // Terminal states follow. CONN_STATE_FINISHED = 100; CONN_STATE_ERROR = 101; CONN_STATE_TIMEOUT = 102; } enum ReadState { READ_STATE_UNKNOWN = 1; READ_STATE_READ = 2; READ_STATE_READ_COMPLETE = 3; READ_STATE_DO_CALLBACK = 4; READ_STATE_HANDLE_ERROR = 5; READ_STATE_ERROR = 100; // Terminal state. } enum WriteState { WRITE_STATE_UNKNOWN = 1; WRITE_STATE_WRITE = 2; WRITE_STATE_WRITE_COMPLETE = 3; WRITE_STATE_DO_CALLBACK = 4; WRITE_STATE_HANDLE_ERROR = 5; // Terminal states follow. WRITE_STATE_ERROR = 100; WRITE_STATE_IDLE = 101; } enum ErrorState { CHANNEL_ERROR_NONE = 1; CHANNEL_ERROR_CHANNEL_NOT_OPEN = 2; CHANNEL_ERROR_AUTHENTICATION_ERROR = 3; CHANNEL_ERROR_CONNECT_ERROR = 4; CHANNEL_ERROR_SOCKET_ERROR = 5; CHANNEL_ERROR_TRANSPORT_ERROR = 6; CHANNEL_ERROR_INVALID_MESSAGE = 7; CHANNEL_ERROR_INVALID_CHANNEL_ID = 8; CHANNEL_ERROR_CONNECT_TIMEOUT = 9; CHANNEL_ERROR_UNKNOWN = 10; } enum ChallengeReplyErrorType { CHALLENGE_REPLY_ERROR_NONE = 1; CHALLENGE_REPLY_ERROR_PEER_CERT_EMPTY = 2; CHALLENGE_REPLY_ERROR_WRONG_PAYLOAD_TYPE = 3; CHALLENGE_REPLY_ERROR_NO_PAYLOAD = 4; CHALLENGE_REPLY_ERROR_PAYLOAD_PARSING_FAILED = 5; CHALLENGE_REPLY_ERROR_MESSAGE_ERROR = 6; CHALLENGE_REPLY_ERROR_NO_RESPONSE = 7; CHALLENGE_REPLY_ERROR_FINGERPRINT_NOT_FOUND = 8; CHALLENGE_REPLY_ERROR_CERT_PARSING_FAILED = 9; CHALLENGE_REPLY_ERROR_CERT_NOT_SIGNED_BY_TRUSTED_CA = 10; CHALLENGE_REPLY_ERROR_CANNOT_EXTRACT_PUBLIC_KEY = 11; CHALLENGE_REPLY_ERROR_SIGNED_BLOBS_MISMATCH = 12; CHALLENGE_REPLY_ERROR_TLS_CERT_VALIDITY_PERIOD_TOO_LONG = 13; CHALLENGE_REPLY_ERROR_TLS_CERT_VALID_START_DATE_IN_FUTURE = 14; CHALLENGE_REPLY_ERROR_TLS_CERT_EXPIRED = 15; CHALLENGE_REPLY_ERROR_CRL_INVALID = 16; CHALLENGE_REPLY_ERROR_CERT_REVOKED = 17; } message SocketEvent { // Required optional EventType type = 1; optional int64 timestamp_micros = 2; optional string details = 3; optional int32 net_return_value = 4; optional string message_namespace = 5; optional ReadyState ready_state = 6; optional ConnectionState connection_state = 7; optional ReadState read_state = 8; optional WriteState write_state = 9; optional ErrorState error_state = 10; optional ChallengeReplyErrorType challenge_reply_error_type = 11; // No longer used. optional int32 nss_error_code = 12; } message AggregatedSocketEvent { optional int32 id = 1; optional int32 endpoint_id = 2; optional ChannelAuth channel_auth_type = 3; repeated SocketEvent socket_event = 4; optional int64 bytes_read = 5; optional int64 bytes_written = 6; } message Log { // Each AggregatedSocketEvent represents events recorded for a socket. repeated AggregatedSocketEvent aggregated_socket_event = 1; // Number of socket log entries evicted by the logger due to size constraints. optional int32 num_evicted_aggregated_socket_events = 2; // Number of event log entries evicted by the logger due to size constraints. optional int32 num_evicted_socket_events = 3; } pychromecast-4.1.0/examples/000077500000000000000000000000001355177132200160415ustar00rootroot00000000000000pychromecast-4.1.0/examples/blocking.py000066400000000000000000000031621355177132200202050ustar00rootroot00000000000000""" Example that shows how the socket client can be used. Functions called in this example are blocking which means that the function doesn't return as long as no result was received. """ import time import sys import logging import pychromecast import pychromecast.controllers.youtube as youtube # Change to the name of your Chromecast CAST_NAME = "Disco room" if '--show-debug' in sys.argv: logging.basicConfig(level=logging.DEBUG) casts = pychromecast.get_chromecasts() if len(casts) == 0: print("No Devices Found") exit() cast = next(cc for cc in casts if cc.device.friendly_name == CAST_NAME) cast.start() print() print(cast.device) time.sleep(1) print() print(cast.status) print() print(cast.media_controller.status) print() if '--show-status-only' in sys.argv: sys.exit() if not cast.is_idle: print("Killing current running app") cast.quit_app() time.sleep(5) print("Playing media") cast.play_media( ("http://commondatastorage.googleapis.com/gtv-videos-bucket/" "sample/BigBuckBunny.mp4"), "video/mp4") t = 0 while True: try: t += 1 if t > 10 and t % 3 == 0: print("Media status", cast.media_controller.status) if t == 15: print("Sending pause command") cast.media_controller.pause() elif t == 20: print("Sending play command") cast.media_controller.play() elif t == 25: print("Sending stop command") cast.media_controller.stop() elif t == 32: cast.quit_app() break time.sleep(1) except KeyboardInterrupt: break pychromecast-4.1.0/examples/dashcast_blocking.py000066400000000000000000000024341355177132200220600ustar00rootroot00000000000000""" Example that shows how the DashCast controller can be used. Functions called in this example are blocking which means that the function doesn't return as long as no result was received. """ import time import sys import logging import pychromecast import pychromecast.controllers.dashcast as dashcast debug = '--show-debug' in sys.argv if debug: logging.basicConfig(level=logging.DEBUG) casts = pychromecast.get_chromecasts() if len(casts) == 0: print("No Devices Found") exit() cast = casts[0] cast.start() d = dashcast.DashCastController() cast.register_handler(d) print() print(cast.device) time.sleep(1) print() print(cast.status) print() print(cast.media_controller.status) print() if not cast.is_idle: print("Killing current running app") cast.quit_app() time.sleep(5) time.sleep(1) # Test that the callback chain works. This should send a message to # load the first url, but immediately after send a message load the # second url. warning_message = 'If you see this on your TV then something is broken' d.load_url('https://home-assistant.io/? ' + warning_message, callback_function=lambda result: d.load_url('https://home-assistant.io/')) # If debugging sleep after running so we can see any error messages. if debug: time.sleep(10) pychromecast-4.1.0/examples/multizone_example.py000066400000000000000000000024431355177132200221570ustar00rootroot00000000000000""" Example on how to use the Multizone (Audio Group) Controller """ import logging import sys import time import pychromecast from pychromecast.controllers.multizone import MultizoneController from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED # Change to the name of your Chromecast CAST_NAME = "Whole house" debug = '--show-debug' in sys.argv if debug: logging.basicConfig(level=logging.DEBUG) class connlistener: def __init__(self, mz): self._mz=mz def new_connection_status(self, connection_status): """Handle reception of a new ConnectionStatus.""" if connection_status.status == 'CONNECTED': self._mz.update_members() class mzlistener: def multizone_member_added(self, uuid): print("New member: {}".format(uuid)) def multizone_member_removed(self, uuid): print("Removed member: {}".format(uuid)) def multizone_status_received(self): print("Members: {}".format(mz.members)) chromecasts = pychromecast.get_chromecasts(timeout=2) cast = next(cc for cc in chromecasts if cc.device.friendly_name == CAST_NAME) mz = MultizoneController(cast.uuid) mz.register_listener(mzlistener()) cast.register_handler(mz) cast.register_connection_listener(connlistener(mz)) cast.wait() while True: time.sleep(1) pychromecast-4.1.0/examples/non_blocking.py000066400000000000000000000042721355177132200210620ustar00rootroot00000000000000""" Example that shows how the socket client can be used. All functions (except get_chromecast()) are non-blocking and return immediately without waiting for the result. You can use that functionality to include pychromecast into your main loop. """ import time import select import sys import logging import pychromecast """ Check for cast.socket_client.get_socket() and handle it with cast.socket_client.run_once() """ def your_main_loop(): t = 1 cast = None def callback(chromecast): chromecast.connect() nonlocal cast cast = chromecast stop_discovery() stop_discovery = pychromecast.get_chromecasts(blocking=False, callback=callback) while True: if cast: polltime = 0.1 can_read, _, _ = select.select([cast.socket_client.get_socket()], [], [], polltime) if can_read: #received something on the socket, handle it with run_once() cast.socket_client.run_once() do_actions(cast, t) t += 1 if(t > 50): break else: print("=> Waiting for cast discovery...") time.sleep(1) """ Your code which is called by main loop """ def do_actions(cast, t): if t == 5: print() print("=> Sending non-blocking play_media command") cast.play_media( ("http://commondatastorage.googleapis.com/gtv-videos-bucket/" "sample/BigBuckBunny.mp4"), "video/mp4") elif t == 30: print() print("=> Sending non-blocking pause command") cast.media_controller.pause() elif t == 35: print() print("=> Sending non-blocking play command") cast.media_controller.play() elif t == 40: print() print("=> Sending non-blocking stop command") cast.media_controller.stop() elif t == 45: print() print("=> Sending non-blocking quit_app command") cast.quit_app() elif t % 4 == 0: print() print("Media status", cast.media_controller.status) if '--show-debug' in sys.argv: logging.basicConfig(level=logging.DEBUG) else: logging.basicConfig(level=logging.INFO) your_main_loop() pychromecast-4.1.0/examples/simple_listener_example.py000066400000000000000000000021331355177132200233230ustar00rootroot00000000000000""" Example showing how to create a simple Chromecast event listener for device and media status events """ import time import pychromecast class StatusListener: def __init__(self, name, cast): self.name = name self.cast = cast def new_cast_status(self, status): print('[',time.ctime(),' - ', self.name,'] status chromecast change:') print(status) class StatusMediaListener: def __init__(self, name, cast): self.name = name self.cast= cast def new_media_status(self, status): print('[',time.ctime(),' - ', self.name,'] status media change:') print(status) chromecasts = pychromecast.get_chromecasts() chromecast = next(cc for cc in chromecasts if cc.device.friendly_name == "Living Room Speaker") chromecast.start() listenerCast = StatusListener(chromecast.name, chromecast) chromecast.register_status_listener(listenerCast) listenerMedia = StatusMediaListener(chromecast.name, chromecast) chromecast.media_controller.register_status_listener(listenerMedia) input('Listening for Chromecast events...\n\n')pychromecast-4.1.0/examples/spotify_example.py000066400000000000000000000071221355177132200216250ustar00rootroot00000000000000""" Example on how to use the Spotify Controller. NOTE: You need to install the spotipy and spotify-token dependencies. This can be done by running the following: pip install spotify-token pip install git+https://github.com/plamere/spotipy.git """ import argparse import http.client as http_client import logging import time import sys import pychromecast from pychromecast.controllers.spotify import SpotifyController import spotify_token as st import spotipy CAST_NAME = "My Chromecast" parser = argparse.ArgumentParser( description="Example on how to use the Spotify Controller.") parser.add_argument('--show-debug', help='Enable debug log', action='store_true') parser.add_argument('--cast', help='Name of cast device (default: "%(default)s")', default=CAST_NAME) parser.add_argument('--user', help='Spotify username', required=True) parser.add_argument('--password', help='Spotify password', required=True) parser.add_argument('--uri', help='Spotify uri(s) (default: "%(default)s")', default=["spotify:track:3Zwu2K0Qa5sT6teCCHPShP"], nargs='+') args = parser.parse_args() if args.show_debug: logging.basicConfig(level=logging.DEBUG) # Uncomment to enable http.client debug log #http_client.HTTPConnection.debuglevel = 1 chromecasts = pychromecast.get_chromecasts() cast = None for _cast in chromecasts: if _cast.name == args.cast: cast = _cast break if not cast: print('No chromecast with name "{}" discovered'.format(args.cast)) print('Discovered casts: {}'.format(chromecasts)) sys.exit(1) print('cast {}'.format(cast)) class ConnListener: def __init__(self, mz): self._mz=mz def new_connection_status(self, connection_status): """Handle reception of a new ConnectionStatus.""" if connection_status.status == 'CONNECTED': self._mz.update_members() class MzListener: def __init__(self): self.got_members=False def multizone_member_added(self, uuid): pass def multizone_member_removed(self, uuid): pass def multizone_status_received(self): self.got_members=True # Wait for connection to the chromecast cast.wait() spotify_device_id = None # Create a spotify token data = st.start_session(args.user, args.password) access_token = data[0] expires = data[1] - int(time.time()) # Create a spotify client client = spotipy.Spotify(auth=access_token) if args.show_debug: spotipy.trace = True spotipy.trace_out = True # Launch the spotify app on the cast we want to cast to sp = SpotifyController(access_token, expires) cast.register_handler(sp) sp.launch_app() if not sp.is_launched and not sp.credential_error: print('Failed to launch spotify controller due to timeout') sys.exit(1) if not sp.is_launched and sp.credential_error: print('Failed to launch spotify controller due to credential error') sys.exit(1) # Query spotify for active devices devices_available = client.devices() # Match active spotify devices with the spotify controller's device id for device in devices_available['devices']: if device['id'] == sp.device: spotify_device_id = device['id'] break if not spotify_device_id: print('No device with id "{}" known by Spotify'.format(sp.device)) print('Known devices: {}'.format(devices_available['devices'])) sys.exit(1) # Start playback if args.uri[0].find('track') > 0: client.start_playback(device_id=spotify_device_id, uris=args.uri) else: client.start_playback(device_id=spotify_device_id, context_uri=args.uri[0]) pychromecast-4.1.0/examples/youtube_example.py000066400000000000000000000010551355177132200216230ustar00rootroot00000000000000""" Example on how to use the YouTube Controller """ import pychromecast from pychromecast.controllers.youtube import YouTubeController # Change to the name of your Chromecast CAST_NAME = "Living Room TV" # Change to the video id of the YouTube video # video id is the last part of the url http://youtube.com/watch?v=video_id VIDEO_ID = "" chromecasts = pychromecast.get_chromecasts() cast = next(cc for cc in chromecasts if cc.device.friendly_name == CAST_NAME) cast.wait() yt = YouTubeController() cast.register_handler(yt) yt.play_video(VIDEO_ID) pychromecast-4.1.0/fabfile.py000066400000000000000000000010131355177132200161600ustar00rootroot00000000000000import os from fabric.decorators import task from fabric.operations import local @task def build(): """ Builds the distribution files """ if not os.path.exists("build"): os.mkdir("build") local("date >> build/log") local("python setup.py sdist >> build/log") local("python setup.py bdist_wheel >> build/log") @task def release(): """ Uploads files to PyPi to create a new release. Note: Requires that files have been built first """ local("twine upload dist/*") pychromecast-4.1.0/pychromecast/000077500000000000000000000000001355177132200167245ustar00rootroot00000000000000pychromecast-4.1.0/pychromecast/__init__.py000066400000000000000000000347341355177132200210500ustar00rootroot00000000000000""" PyChromecast: remote control your Chromecast """ import logging import fnmatch # pylint: disable=wildcard-import import threading from .config import * # noqa from .error import * # noqa from . import socket_client from .discovery import discover_chromecasts, start_discovery, stop_discovery from .dial import ( get_device_status, reboot, DeviceStatus, CAST_TYPES, CAST_TYPE_CHROMECAST, ) from .controllers.media import STREAM_TYPE_BUFFERED # noqa __all__ = ("__version__", "__version_info__", "get_chromecasts", "Chromecast") __version_info__ = ("0", "7", "6") __version__ = ".".join(__version_info__) IDLE_APP_ID = "E8C28D3C" IGNORE_CEC = [] _LOGGER = logging.getLogger(__name__) def _get_chromecast_from_host( host, tries=None, retry_wait=None, timeout=None, blocking=True ): """Creates a Chromecast object from a zeroconf host.""" # Build device status from the mDNS info, this information is # the primary source and the remaining will be fetched # later on. ip_address, port, uuid, model_name, friendly_name = host _LOGGER.debug("_get_chromecast_from_host %s", host) cast_type = CAST_TYPES.get(model_name.lower(), CAST_TYPE_CHROMECAST) device = DeviceStatus( friendly_name=friendly_name, model_name=model_name, manufacturer=None, uuid=uuid, cast_type=cast_type, ) return Chromecast( host=ip_address, port=port, device=device, tries=tries, timeout=timeout, retry_wait=retry_wait, blocking=blocking, ) def _get_chromecast_from_service( services, tries=None, retry_wait=None, timeout=None, blocking=True ): """Creates a Chromecast object from a zeroconf service.""" # Build device status from the mDNS service name info, this # information is the primary source and the remaining will be # fetched later on. services, zconf, uuid, model_name, friendly_name = services _LOGGER.debug("_get_chromecast_from_service %s", services) cast_type = CAST_TYPES.get(model_name.lower(), CAST_TYPE_CHROMECAST) device = DeviceStatus( friendly_name=friendly_name, model_name=model_name, manufacturer=None, uuid=uuid, cast_type=cast_type, ) return Chromecast( host=None, device=device, tries=tries, timeout=timeout, retry_wait=retry_wait, blocking=blocking, services=services, zconf=zconf, ) # pylint: disable=too-many-locals def get_chromecasts( tries=None, retry_wait=None, timeout=None, blocking=True, callback=None ): """ Searches the network for chromecast devices. If blocking = True, returns a list of discovered chromecast devices. If blocking = False, triggers a callback for each discovered chromecast, and returns a function which can be executed to stop discovery. May return an empty list if no chromecasts were found. Tries is specified if you want to limit the number of times the underlying socket associated with your Chromecast objects will retry connecting if connection is lost or it fails to connect in the first place. The number of seconds spent between each retry can be defined by passing the retry_wait parameter, the default is to wait 5 seconds. """ if blocking: # Thread blocking chromecast discovery hosts = discover_chromecasts() cc_list = [] for host in hosts: try: cc_list.append( _get_chromecast_from_host( host, tries=tries, retry_wait=retry_wait, timeout=timeout, blocking=blocking, ) ) except ChromecastConnectionError: # noqa pass return cc_list # Callback based chromecast discovery if not callable(callback): raise ValueError("Nonblocking discovery requires a callback function.") def internal_callback(name): """Called when zeroconf has discovered a new chromecast.""" try: callback( _get_chromecast_from_host( listener.services[name], tries=tries, retry_wait=retry_wait, timeout=timeout, blocking=blocking, ) ) except ChromecastConnectionError: # noqa pass def internal_stop(): """Stops discovery of new chromecasts.""" stop_discovery(browser) listener, browser = start_discovery(internal_callback) return internal_stop # pylint: disable=too-many-instance-attributes, too-many-public-methods class Chromecast: """ Class to interface with a ChromeCast. :param port: The port to use when connecting to the device, set to None to use the default of 8009. Special devices such as Cast Groups may return a different port number so we need to use that. :param device: DeviceStatus with initial information for the device. :type device: pychromecast.dial.DeviceStatus :param tries: Number of retries to perform if the connection fails. None for inifinite retries. :param timeout: A floating point number specifying the socket timeout in seconds. None means to use the default which is 30 seconds. :param retry_wait: A floating point number specifying how many seconds to wait between each retry. None means to use the default which is 5 seconds. """ def __init__(self, host, port=None, device=None, **kwargs): tries = kwargs.pop("tries", None) timeout = kwargs.pop("timeout", None) retry_wait = kwargs.pop("retry_wait", None) blocking = kwargs.pop("blocking", True) services = kwargs.pop("services", None) zconf = kwargs.pop("zconf", True) self.logger = logging.getLogger(__name__) # Resolve host to IP address self._services = services self.host = host self.port = port or 8009 self.logger.info("Querying device status") self.device = device if device: dev_status = get_device_status(self.host, services, zconf) if dev_status: # Values from `device` have priority over `dev_status` # as they come from the dial information. # `dev_status` may add extra information such as `manufacturer` # which dial does not supply self.device = DeviceStatus( friendly_name=(device.friendly_name or dev_status.friendly_name), model_name=(device.model_name or dev_status.model_name), manufacturer=(device.manufacturer or dev_status.manufacturer), uuid=(device.uuid or dev_status.uuid), cast_type=(device.cast_type or dev_status.cast_type), ) else: self.device = device else: self.device = get_device_status(self.host, services, zconf) if not self.device: raise ChromecastConnectionError( # noqa "Could not connect to {}:{}".format(self.host, self.port) ) self.status = None self.status_event = threading.Event() self.socket_client = socket_client.SocketClient( host, port=port, cast_type=self.device.cast_type, tries=tries, timeout=timeout, retry_wait=retry_wait, blocking=blocking, services=services, zconf=zconf, ) receiver_controller = self.socket_client.receiver_controller receiver_controller.register_status_listener(self) # Forward these methods self.set_volume = receiver_controller.set_volume self.set_volume_muted = receiver_controller.set_volume_muted self.play_media = self.socket_client.media_controller.play_media self.register_handler = self.socket_client.register_handler self.register_status_listener = receiver_controller.register_status_listener self.register_launch_error_listener = ( receiver_controller.register_launch_error_listener ) self.register_connection_listener = ( self.socket_client.register_connection_listener ) @property def ignore_cec(self): """ Returns whether the CEC data should be ignored. """ return self.device is not None and any( [ fnmatch.fnmatchcase(self.device.friendly_name, pattern) for pattern in IGNORE_CEC ] ) @property def is_idle(self): """ Returns if there is currently an app running. """ return ( self.status is None or self.app_id in (None, IDLE_APP_ID) or (not self.status.is_active_input and not self.ignore_cec) ) @property def uuid(self): """ Returns the unique UUID of the Chromecast device. """ return self.device.uuid @property def name(self): """ Returns the friendly name set for the Chromecast device. This is the name that the end-user chooses for the cast device. """ return self.device.friendly_name @property def uri(self): """ Returns the device URI (ip:port) """ return "{}:{}".format(self.host, self.port) @property def model_name(self): """ Returns the model name of the Chromecast device. """ return self.device.model_name @property def cast_type(self): """ Returns the type of the Chromecast device. This is one of CAST_TYPE_CHROMECAST for regular Chromecast device, CAST_TYPE_AUDIO for Chromecast devices that only support audio and CAST_TYPE_GROUP for virtual a Chromecast device that groups together two or more cast (Audio for now) devices. :rtype: str """ return self.device.cast_type @property def app_id(self): """ Returns the current app_id. """ return self.status.app_id if self.status else None @property def app_display_name(self): """ Returns the name of the current running app. """ return self.status.display_name if self.status else None @property def media_controller(self): """ Returns the media controller. """ return self.socket_client.media_controller def new_cast_status(self, status): """ Called when a new status received from the Chromecast. """ self.status = status if status: self.status_event.set() def start_app(self, app_id, force_launch=False): """ Start an app on the Chromecast. """ self.logger.info("Starting app %s", app_id) self.socket_client.receiver_controller.launch_app(app_id, force_launch) def quit_app(self): """ Tells the Chromecast to quit current app_id. """ self.logger.info("Quiting current app") self.socket_client.receiver_controller.stop_app() def reboot(self): """ Reboots the Chromecast. """ reboot(self.host) def volume_up(self, delta=0.1): """ Increment volume by 0.1 (or delta) unless it is already maxed. Returns the new volume. """ if delta <= 0: raise ValueError( "volume delta must be greater than zero, not {}".format(delta) ) return self.set_volume(self.status.volume_level + delta) def volume_down(self, delta=0.1): """ Decrement the volume by 0.1 (or delta) unless it is already 0. Returns the new volume. """ if delta <= 0: raise ValueError( "volume delta must be greater than zero, not {}".format(delta) ) return self.set_volume(self.status.volume_level - delta) def wait(self, timeout=None): """ Waits until the cast device is ready for communication. The device is ready as soon a status message has been received. If the worker thread is not already running, it will be started. If the status has already been received then the method returns immediately. :param timeout: a floating point number specifying a timeout for the operation in seconds (or fractions thereof). Or None to block forever. """ if not self.socket_client.isAlive(): self.socket_client.start() self.status_event.wait(timeout=timeout) def connect(self): """ Connect to the chromecast. Must only be called if the worker thread will not be started. """ self.socket_client.connect() def disconnect(self, timeout=None, blocking=True): """ Disconnects the chromecast and waits for it to terminate. :param timeout: a floating point number specifying a timeout for the operation in seconds (or fractions thereof). Or None to block forever. :param blocking: If True it will block until the disconnection is complete, otherwise it will return immediately. """ self.socket_client.disconnect() if blocking: self.join(timeout=timeout) def join(self, timeout=None): """ Blocks the thread of the caller until the chromecast connection is stopped. :param timeout: a floating point number specifying a timeout for the operation in seconds (or fractions thereof). Or None to block forever. """ self.socket_client.join(timeout=timeout) def start(self): """ Start the chromecast connection's worker thread. """ self.socket_client.start() def __del__(self): try: self.socket_client.stop.set() except AttributeError: pass def __repr__(self): txt = "Chromecast({!r}, port={!r}, device={!r})".format( self.host, self.port, self.device ) return txt def __unicode__(self): return "Chromecast({}, {}, {}, {}, {})".format( self.host, self.port, self.device.friendly_name, self.device.model_name, self.device.manufacturer, ) pychromecast-4.1.0/pychromecast/authority_keys_pb2.py000066400000000000000000000075421355177132200231340ustar00rootroot00000000000000# Generated by the protocol buffer compiler. DO NOT EDIT! # source: authority_keys.proto import sys _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database from google.protobuf import descriptor_pb2 # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor.FileDescriptor( name='authority_keys.proto', package='extensions.api.cast_channel.proto', syntax='proto2', serialized_pb=_b('\n\x14\x61uthority_keys.proto\x12!extensions.api.cast_channel.proto\"\x83\x01\n\rAuthorityKeys\x12\x42\n\x04keys\x18\x01 \x03(\x0b\x32\x34.extensions.api.cast_channel.proto.AuthorityKeys.Key\x1a.\n\x03Key\x12\x13\n\x0b\x66ingerprint\x18\x01 \x02(\x0c\x12\x12\n\npublic_key\x18\x02 \x02(\x0c\x42\x02H\x03') ) _sym_db.RegisterFileDescriptor(DESCRIPTOR) _AUTHORITYKEYS_KEY = _descriptor.Descriptor( name='Key', full_name='extensions.api.cast_channel.proto.AuthorityKeys.Key', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='fingerprint', full_name='extensions.api.cast_channel.proto.AuthorityKeys.Key.fingerprint', index=0, number=1, type=12, cpp_type=9, label=2, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='public_key', full_name='extensions.api.cast_channel.proto.AuthorityKeys.Key.public_key', index=1, number=2, type=12, cpp_type=9, label=2, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=145, serialized_end=191, ) _AUTHORITYKEYS = _descriptor.Descriptor( name='AuthorityKeys', full_name='extensions.api.cast_channel.proto.AuthorityKeys', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='keys', full_name='extensions.api.cast_channel.proto.AuthorityKeys.keys', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[_AUTHORITYKEYS_KEY, ], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=60, serialized_end=191, ) _AUTHORITYKEYS_KEY.containing_type = _AUTHORITYKEYS _AUTHORITYKEYS.fields_by_name['keys'].message_type = _AUTHORITYKEYS_KEY DESCRIPTOR.message_types_by_name['AuthorityKeys'] = _AUTHORITYKEYS AuthorityKeys = _reflection.GeneratedProtocolMessageType('AuthorityKeys', (_message.Message,), dict( Key = _reflection.GeneratedProtocolMessageType('Key', (_message.Message,), dict( DESCRIPTOR = _AUTHORITYKEYS_KEY, __module__ = 'authority_keys_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.proto.AuthorityKeys.Key) )) , DESCRIPTOR = _AUTHORITYKEYS, __module__ = 'authority_keys_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.proto.AuthorityKeys) )) _sym_db.RegisterMessage(AuthorityKeys) _sym_db.RegisterMessage(AuthorityKeys.Key) DESCRIPTOR.has_options = True DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('H\003')) # @@protoc_insertion_point(module_scope) pychromecast-4.1.0/pychromecast/cast_channel_pb2.py000066400000000000000000000451571355177132200224770ustar00rootroot00000000000000# Generated by the protocol buffer compiler. DO NOT EDIT! # source: cast_channel.proto import sys _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) from google.protobuf.internal import enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database from google.protobuf import descriptor_pb2 # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor.FileDescriptor( name='cast_channel.proto', package='extensions.api.cast_channel', syntax='proto2', serialized_pb=_b('\n\x12\x63\x61st_channel.proto\x12\x1b\x65xtensions.api.cast_channel\"\xe3\x02\n\x0b\x43\x61stMessage\x12R\n\x10protocol_version\x18\x01 \x02(\x0e\x32\x38.extensions.api.cast_channel.CastMessage.ProtocolVersion\x12\x11\n\tsource_id\x18\x02 \x02(\t\x12\x16\n\x0e\x64\x65stination_id\x18\x03 \x02(\t\x12\x11\n\tnamespace\x18\x04 \x02(\t\x12J\n\x0cpayload_type\x18\x05 \x02(\x0e\x32\x34.extensions.api.cast_channel.CastMessage.PayloadType\x12\x14\n\x0cpayload_utf8\x18\x06 \x01(\t\x12\x16\n\x0epayload_binary\x18\x07 \x01(\x0c\"!\n\x0fProtocolVersion\x12\x0e\n\nCASTV2_1_0\x10\x00\"%\n\x0bPayloadType\x12\n\n\x06STRING\x10\x00\x12\n\n\x06\x42INARY\x10\x01\"\xce\x01\n\rAuthChallenge\x12]\n\x13signature_algorithm\x18\x01 \x01(\x0e\x32/.extensions.api.cast_channel.SignatureAlgorithm:\x0fRSASSA_PKCS1v15\x12\x14\n\x0csender_nonce\x18\x02 \x01(\x0c\x12H\n\x0ehash_algorithm\x18\x03 \x01(\x0e\x32*.extensions.api.cast_channel.HashAlgorithm:\x04SHA1\"\xb0\x02\n\x0c\x41uthResponse\x12\x11\n\tsignature\x18\x01 \x02(\x0c\x12\x1f\n\x17\x63lient_auth_certificate\x18\x02 \x02(\x0c\x12 \n\x18intermediate_certificate\x18\x03 \x03(\x0c\x12]\n\x13signature_algorithm\x18\x04 \x01(\x0e\x32/.extensions.api.cast_channel.SignatureAlgorithm:\x0fRSASSA_PKCS1v15\x12\x14\n\x0csender_nonce\x18\x05 \x01(\x0c\x12H\n\x0ehash_algorithm\x18\x06 \x01(\x0e\x32*.extensions.api.cast_channel.HashAlgorithm:\x04SHA1\x12\x0b\n\x03\x63rl\x18\x07 \x01(\x0c\"\xa3\x01\n\tAuthError\x12\x44\n\nerror_type\x18\x01 \x02(\x0e\x32\x30.extensions.api.cast_channel.AuthError.ErrorType\"P\n\tErrorType\x12\x12\n\x0eINTERNAL_ERROR\x10\x00\x12\n\n\x06NO_TLS\x10\x01\x12#\n\x1fSIGNATURE_ALGORITHM_UNAVAILABLE\x10\x02\"\xc6\x01\n\x11\x44\x65viceAuthMessage\x12=\n\tchallenge\x18\x01 \x01(\x0b\x32*.extensions.api.cast_channel.AuthChallenge\x12;\n\x08response\x18\x02 \x01(\x0b\x32).extensions.api.cast_channel.AuthResponse\x12\x35\n\x05\x65rror\x18\x03 \x01(\x0b\x32&.extensions.api.cast_channel.AuthError*J\n\x12SignatureAlgorithm\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x13\n\x0fRSASSA_PKCS1v15\x10\x01\x12\x0e\n\nRSASSA_PSS\x10\x02*%\n\rHashAlgorithm\x12\x08\n\x04SHA1\x10\x00\x12\n\n\x06SHA256\x10\x01\x42\x02H\x03') ) _sym_db.RegisterFileDescriptor(DESCRIPTOR) _SIGNATUREALGORITHM = _descriptor.EnumDescriptor( name='SignatureAlgorithm', full_name='extensions.api.cast_channel.SignatureAlgorithm', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='UNSPECIFIED', index=0, number=0, options=None, type=None), _descriptor.EnumValueDescriptor( name='RSASSA_PKCS1v15', index=1, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='RSASSA_PSS', index=2, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=1292, serialized_end=1366, ) _sym_db.RegisterEnumDescriptor(_SIGNATUREALGORITHM) SignatureAlgorithm = enum_type_wrapper.EnumTypeWrapper(_SIGNATUREALGORITHM) _HASHALGORITHM = _descriptor.EnumDescriptor( name='HashAlgorithm', full_name='extensions.api.cast_channel.HashAlgorithm', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='SHA1', index=0, number=0, options=None, type=None), _descriptor.EnumValueDescriptor( name='SHA256', index=1, number=1, options=None, type=None), ], containing_type=None, options=None, serialized_start=1368, serialized_end=1405, ) _sym_db.RegisterEnumDescriptor(_HASHALGORITHM) HashAlgorithm = enum_type_wrapper.EnumTypeWrapper(_HASHALGORITHM) UNSPECIFIED = 0 RSASSA_PKCS1v15 = 1 RSASSA_PSS = 2 SHA1 = 0 SHA256 = 1 _CASTMESSAGE_PROTOCOLVERSION = _descriptor.EnumDescriptor( name='ProtocolVersion', full_name='extensions.api.cast_channel.CastMessage.ProtocolVersion', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='CASTV2_1_0', index=0, number=0, options=None, type=None), ], containing_type=None, options=None, serialized_start=335, serialized_end=368, ) _sym_db.RegisterEnumDescriptor(_CASTMESSAGE_PROTOCOLVERSION) _CASTMESSAGE_PAYLOADTYPE = _descriptor.EnumDescriptor( name='PayloadType', full_name='extensions.api.cast_channel.CastMessage.PayloadType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='STRING', index=0, number=0, options=None, type=None), _descriptor.EnumValueDescriptor( name='BINARY', index=1, number=1, options=None, type=None), ], containing_type=None, options=None, serialized_start=370, serialized_end=407, ) _sym_db.RegisterEnumDescriptor(_CASTMESSAGE_PAYLOADTYPE) _AUTHERROR_ERRORTYPE = _descriptor.EnumDescriptor( name='ErrorType', full_name='extensions.api.cast_channel.AuthError.ErrorType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='INTERNAL_ERROR', index=0, number=0, options=None, type=None), _descriptor.EnumValueDescriptor( name='NO_TLS', index=1, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='SIGNATURE_ALGORITHM_UNAVAILABLE', index=2, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=1009, serialized_end=1089, ) _sym_db.RegisterEnumDescriptor(_AUTHERROR_ERRORTYPE) _CASTMESSAGE = _descriptor.Descriptor( name='CastMessage', full_name='extensions.api.cast_channel.CastMessage', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='protocol_version', full_name='extensions.api.cast_channel.CastMessage.protocol_version', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='source_id', full_name='extensions.api.cast_channel.CastMessage.source_id', index=1, number=2, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='destination_id', full_name='extensions.api.cast_channel.CastMessage.destination_id', index=2, number=3, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='namespace', full_name='extensions.api.cast_channel.CastMessage.namespace', index=3, number=4, type=9, cpp_type=9, label=2, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='payload_type', full_name='extensions.api.cast_channel.CastMessage.payload_type', index=4, number=5, type=14, cpp_type=8, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='payload_utf8', full_name='extensions.api.cast_channel.CastMessage.payload_utf8', index=5, number=6, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='payload_binary', full_name='extensions.api.cast_channel.CastMessage.payload_binary', index=6, number=7, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _CASTMESSAGE_PROTOCOLVERSION, _CASTMESSAGE_PAYLOADTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=52, serialized_end=407, ) _AUTHCHALLENGE = _descriptor.Descriptor( name='AuthChallenge', full_name='extensions.api.cast_channel.AuthChallenge', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='signature_algorithm', full_name='extensions.api.cast_channel.AuthChallenge.signature_algorithm', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=True, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='sender_nonce', full_name='extensions.api.cast_channel.AuthChallenge.sender_nonce', index=1, number=2, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='hash_algorithm', full_name='extensions.api.cast_channel.AuthChallenge.hash_algorithm', index=2, number=3, type=14, cpp_type=8, label=1, has_default_value=True, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=410, serialized_end=616, ) _AUTHRESPONSE = _descriptor.Descriptor( name='AuthResponse', full_name='extensions.api.cast_channel.AuthResponse', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='signature', full_name='extensions.api.cast_channel.AuthResponse.signature', index=0, number=1, type=12, cpp_type=9, label=2, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='client_auth_certificate', full_name='extensions.api.cast_channel.AuthResponse.client_auth_certificate', index=1, number=2, type=12, cpp_type=9, label=2, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='intermediate_certificate', full_name='extensions.api.cast_channel.AuthResponse.intermediate_certificate', index=2, number=3, type=12, cpp_type=9, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='signature_algorithm', full_name='extensions.api.cast_channel.AuthResponse.signature_algorithm', index=3, number=4, type=14, cpp_type=8, label=1, has_default_value=True, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='sender_nonce', full_name='extensions.api.cast_channel.AuthResponse.sender_nonce', index=4, number=5, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='hash_algorithm', full_name='extensions.api.cast_channel.AuthResponse.hash_algorithm', index=5, number=6, type=14, cpp_type=8, label=1, has_default_value=True, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='crl', full_name='extensions.api.cast_channel.AuthResponse.crl', index=6, number=7, type=12, cpp_type=9, label=1, has_default_value=False, default_value=_b(""), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=619, serialized_end=923, ) _AUTHERROR = _descriptor.Descriptor( name='AuthError', full_name='extensions.api.cast_channel.AuthError', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='error_type', full_name='extensions.api.cast_channel.AuthError.error_type', index=0, number=1, type=14, cpp_type=8, label=2, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ _AUTHERROR_ERRORTYPE, ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=926, serialized_end=1089, ) _DEVICEAUTHMESSAGE = _descriptor.Descriptor( name='DeviceAuthMessage', full_name='extensions.api.cast_channel.DeviceAuthMessage', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='challenge', full_name='extensions.api.cast_channel.DeviceAuthMessage.challenge', index=0, number=1, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='response', full_name='extensions.api.cast_channel.DeviceAuthMessage.response', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='error', full_name='extensions.api.cast_channel.DeviceAuthMessage.error', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=1092, serialized_end=1290, ) _CASTMESSAGE.fields_by_name['protocol_version'].enum_type = _CASTMESSAGE_PROTOCOLVERSION _CASTMESSAGE.fields_by_name['payload_type'].enum_type = _CASTMESSAGE_PAYLOADTYPE _CASTMESSAGE_PROTOCOLVERSION.containing_type = _CASTMESSAGE _CASTMESSAGE_PAYLOADTYPE.containing_type = _CASTMESSAGE _AUTHCHALLENGE.fields_by_name['signature_algorithm'].enum_type = _SIGNATUREALGORITHM _AUTHCHALLENGE.fields_by_name['hash_algorithm'].enum_type = _HASHALGORITHM _AUTHRESPONSE.fields_by_name['signature_algorithm'].enum_type = _SIGNATUREALGORITHM _AUTHRESPONSE.fields_by_name['hash_algorithm'].enum_type = _HASHALGORITHM _AUTHERROR.fields_by_name['error_type'].enum_type = _AUTHERROR_ERRORTYPE _AUTHERROR_ERRORTYPE.containing_type = _AUTHERROR _DEVICEAUTHMESSAGE.fields_by_name['challenge'].message_type = _AUTHCHALLENGE _DEVICEAUTHMESSAGE.fields_by_name['response'].message_type = _AUTHRESPONSE _DEVICEAUTHMESSAGE.fields_by_name['error'].message_type = _AUTHERROR DESCRIPTOR.message_types_by_name['CastMessage'] = _CASTMESSAGE DESCRIPTOR.message_types_by_name['AuthChallenge'] = _AUTHCHALLENGE DESCRIPTOR.message_types_by_name['AuthResponse'] = _AUTHRESPONSE DESCRIPTOR.message_types_by_name['AuthError'] = _AUTHERROR DESCRIPTOR.message_types_by_name['DeviceAuthMessage'] = _DEVICEAUTHMESSAGE DESCRIPTOR.enum_types_by_name['SignatureAlgorithm'] = _SIGNATUREALGORITHM DESCRIPTOR.enum_types_by_name['HashAlgorithm'] = _HASHALGORITHM CastMessage = _reflection.GeneratedProtocolMessageType('CastMessage', (_message.Message,), dict( DESCRIPTOR = _CASTMESSAGE, __module__ = 'cast_channel_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.CastMessage) )) _sym_db.RegisterMessage(CastMessage) AuthChallenge = _reflection.GeneratedProtocolMessageType('AuthChallenge', (_message.Message,), dict( DESCRIPTOR = _AUTHCHALLENGE, __module__ = 'cast_channel_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.AuthChallenge) )) _sym_db.RegisterMessage(AuthChallenge) AuthResponse = _reflection.GeneratedProtocolMessageType('AuthResponse', (_message.Message,), dict( DESCRIPTOR = _AUTHRESPONSE, __module__ = 'cast_channel_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.AuthResponse) )) _sym_db.RegisterMessage(AuthResponse) AuthError = _reflection.GeneratedProtocolMessageType('AuthError', (_message.Message,), dict( DESCRIPTOR = _AUTHERROR, __module__ = 'cast_channel_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.AuthError) )) _sym_db.RegisterMessage(AuthError) DeviceAuthMessage = _reflection.GeneratedProtocolMessageType('DeviceAuthMessage', (_message.Message,), dict( DESCRIPTOR = _DEVICEAUTHMESSAGE, __module__ = 'cast_channel_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.DeviceAuthMessage) )) _sym_db.RegisterMessage(DeviceAuthMessage) DESCRIPTOR.has_options = True DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('H\003')) # @@protoc_insertion_point(module_scope) pychromecast-4.1.0/pychromecast/config.py000066400000000000000000000021351355177132200205440ustar00rootroot00000000000000""" Data and methods to retrieve app specific configuration """ import json import requests APP_BACKDROP = "E8C28D3C" APP_YOUTUBE = "233637DE" APP_MEDIA_RECEIVER = "CC1AD845" APP_PLEX = "06ee44ee-e7e3-4249-83b6-f5d0b6f07f34_1" APP_DASHCAST = "84912283" APP_SPOTIFY = "CC32E753" APP_HOME_ASSISTANT = "B12CE3CA" def get_possible_app_ids(): """ Returns all possible app ids. """ try: req = requests.get( "https://clients3.google.com/cast/chromecast/device/baseconfig" ) data = json.loads(req.text[4:]) return [app["app_id"] for app in data["applications"]] + data["enabled_app_ids"] except ValueError: # If json fails to parse return [] def get_app_config(app_id): """ Get specific configuration for 'app_id'. """ try: req = requests.get( ("https://clients3.google.com/" "cast/chromecast/device/app?a={}").format( app_id ) ) return json.loads(req.text[4:]) if req.status_code == 200 else {} except ValueError: # If json fails to parse return {} pychromecast-4.1.0/pychromecast/controllers/000077500000000000000000000000001355177132200212725ustar00rootroot00000000000000pychromecast-4.1.0/pychromecast/controllers/__init__.py000066400000000000000000000077661355177132200234230ustar00rootroot00000000000000""" Provides controllers to handle specific namespaces in Chromecast communication. """ import logging from ..error import UnsupportedNamespace, ControllerNotRegistered class BaseController: """ ABC for namespace controllers. """ def __init__(self, namespace, supporting_app_id=None, target_platform=False): """ Initialize the controller. namespace: the namespace this controller will act on supporting_app_id: app to be launched if app is running with unsupported namespace. target_platform: set to True if you target the platform instead of current app. """ self.namespace = namespace self.supporting_app_id = supporting_app_id self.target_platform = target_platform self._socket_client = None self._message_func = None self.logger = logging.getLogger(__name__) @property def is_active(self): """ True if the controller is connected to a socket client and the Chromecast is running an app that supports this controller. """ return ( self._socket_client is not None and self.namespace in self._socket_client.app_namespaces ) def launch(self, callback_function=None): """ If set, launches app related to the controller. """ self._check_registered() self._socket_client.receiver_controller.launch_app( self.supporting_app_id, callback_function=callback_function ) def registered(self, socket_client): """ Called when a controller is registered. """ self._socket_client = socket_client if self.target_platform: self._message_func = self._socket_client.send_platform_message else: self._message_func = self._socket_client.send_app_message def channel_connected(self): """ Called when a channel has been openend that supports the namespace of this controller. """ def channel_disconnected(self): """ Called when a channel is disconnected. """ def send_message(self, data, inc_session_id=False, callback_function=None): """ Send a message on this namespace to the Chromecast. Ensures app is loaded. Will raise a NotConnected exception if not connected. """ self._check_registered() if ( not self.target_platform and self.namespace not in self._socket_client.app_namespaces ): if self.supporting_app_id is not None: self.launch( callback_function=lambda: self.send_message_nocheck( data, inc_session_id, callback_function ) ) return raise UnsupportedNamespace( ("Namespace {} is not supported by running" "application.").format( self.namespace ) ) self.send_message_nocheck(data, inc_session_id, callback_function) def send_message_nocheck(self, data, inc_session_id=False, callback_function=None): """Send a message.""" self._message_func(self.namespace, data, inc_session_id, callback_function) # pylint: disable=unused-argument,no-self-use def receive_message(self, message, data): """ Called when a message is received that matches the namespace. Returns boolean indicating if message was handled. """ return False def tear_down(self): """ Called when we are shutting down. """ self._socket_client = None self._message_func = None def _check_registered(self): """ Helper method to see if we are registered with a Cast object. """ if self._socket_client is None: raise ControllerNotRegistered( ( "Trying to use the controller without it being registered " "with a Cast object." ) ) pychromecast-4.1.0/pychromecast/controllers/dashcast.py000066400000000000000000000040341355177132200234370ustar00rootroot00000000000000""" Controller to interface with the DashCast app namespace. """ from ..config import APP_DASHCAST from . import BaseController APP_NAMESPACE = "urn:x-cast:com.madmod.dashcast" class DashCastController(BaseController): """ Controller to interact with DashCast app namespace. """ # pylint: disable=useless-super-delegation # The pylint rule useless-super-delegation doesn't realize # we are setting default values here. def __init__(self, appNamespace=APP_NAMESPACE, appId=APP_DASHCAST): super(DashCastController, self).__init__(appNamespace, appId) # pylint: enable=useless-super-delegation def receive_message(self, message, data): """ Called when a load complete message is received. This is currently un-used by this controller. It is implemented so that we don't get "Message unhandled" warnings. In the future it might be used to update a public status object like the media controller does. """ # Indicate that the message was successfully handled. return True def load_url(self, url, force=False, reload_seconds=0, callback_function=None): """ Starts loading a URL with an optional reload time in seconds. Setting force to True may load pages which block iframe embedding, but will prevent reload from working and will cause calls to load_url() to reload the app. """ def launch_callback(): """Loads requested URL after app launched.""" should_reload = not force and reload_seconds not in (0, None) reload_milliseconds = 0 if not should_reload else reload_seconds * 1000 msg = { "url": url, "force": force, "reload": should_reload, "reload_time": reload_milliseconds, } self.send_message( msg, inc_session_id=True, callback_function=callback_function ) self.launch(callback_function=launch_callback) pychromecast-4.1.0/pychromecast/controllers/homeassistant.py000066400000000000000000000062511355177132200245320ustar00rootroot00000000000000""" Controller to interface with Home Assistant """ from ..config import APP_HOME_ASSISTANT from . import BaseController APP_NAMESPACE = "urn:x-cast:com.nabucasa.hast" class HomeAssistantController(BaseController): """ Controller to interact with Home Assistant. """ def __init__( self, hass_url, client_id, refresh_token, app_namespace=APP_NAMESPACE, app_id=APP_HOME_ASSISTANT, ): super().__init__(app_namespace, app_id) self.hass_url = hass_url self.client_id = client_id self.refresh_token = refresh_token # { # connected: boolean; # showDemo: boolean; # hassUrl?: string; # lovelacePath?: string | number | null; # } self.status = None self._on_connect = [] @property def hass_connected(self): """Return if connected to Home Assistant.""" return ( self.status is not None and self.status["connected"] and self.status["hassUrl"] == self.hass_url ) def channel_connected(self): """ Called when a channel has been openend that supports the namespace of this controller. """ self.get_status() def channel_disconnected(self): """ Called when a channel is disconnected. """ self.status = None def receive_message(self, message, data): """Called when a message is received.""" if data.get("type") == "receiver_status": was_connected = self.hass_connected self.status = data if was_connected or not self.hass_connected: return True # We just got connected, call the callbacks. while self._on_connect: self._on_connect.pop()() return True return False def connect_hass(self, callback_function=None): """Connect to Home Assistant.""" self._on_connect.append(callback_function) self.send_message( { "type": "connect", "refreshToken": self.refresh_token, "clientId": self.client_id, "hassUrl": self.hass_url, } ) def show_demo(self): """Show the demo.""" self.send_message({"type": "show_demo"}) def get_status(self, callback_function=None): """Get status of Home Assistant Cast.""" self.send_connected_message( {"type": "get_status"}, callback_function=callback_function ) def show_lovelace_view(self, view_path, callback_function=None): """Show a Lovelace UI.""" self.send_connected_message( {"type": "show_lovelace_view", "viewPath": view_path}, callback_function=callback_function, ) def send_connected_message(self, data, callback_function=None): """Send a message to a connected Home Assistant Cast""" if self.hass_connected: self.send_message_nocheck(data, callback_function=callback_function) return self.connect_hass( lambda: self.send_message_nocheck(data, callback_function=callback_function) ) pychromecast-4.1.0/pychromecast/controllers/media.py000066400000000000000000000504601355177132200227300ustar00rootroot00000000000000""" Provides a controller for controlling the default media players on the Chromecast. """ from datetime import datetime import logging from collections import namedtuple import threading from ..config import APP_MEDIA_RECEIVER from . import BaseController STREAM_TYPE_UNKNOWN = "UNKNOWN" STREAM_TYPE_BUFFERED = "BUFFERED" STREAM_TYPE_LIVE = "LIVE" MEDIA_PLAYER_STATE_PLAYING = "PLAYING" MEDIA_PLAYER_STATE_BUFFERING = "BUFFERING" MEDIA_PLAYER_STATE_PAUSED = "PAUSED" MEDIA_PLAYER_STATE_IDLE = "IDLE" MEDIA_PLAYER_STATE_UNKNOWN = "UNKNOWN" MESSAGE_TYPE = "type" TYPE_EDIT_TRACKS_INFO = "EDIT_TRACKS_INFO" TYPE_GET_STATUS = "GET_STATUS" TYPE_LOAD = "LOAD" TYPE_MEDIA_STATUS = "MEDIA_STATUS" TYPE_PAUSE = "PAUSE" TYPE_PLAY = "PLAY" TYPE_QUEUE_NEXT = "QUEUE_NEXT" TYPE_QUEUE_PREV = "QUEUE_PREV" TYPE_SEEK = "SEEK" TYPE_STOP = "STOP" METADATA_TYPE_GENERIC = 0 METADATA_TYPE_TVSHOW = 1 METADATA_TYPE_MOVIE = 2 METADATA_TYPE_MUSICTRACK = 3 METADATA_TYPE_PHOTO = 4 # From www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js CMD_SUPPORT_PAUSE = 1 CMD_SUPPORT_SEEK = 2 CMD_SUPPORT_STREAM_VOLUME = 4 CMD_SUPPORT_STREAM_MUTE = 8 # ALL_BASIC_MEDIA = PAUSE | SEEK | VOLUME | MUTE | EDIT_TRACKS | PLAYBACK_RATE CMD_SUPPORT_ALL_BASIC_MEDIA = 12303 CMD_SUPPORT_QUEUE_NEXT = 64 CMD_SUPPORT_QUEUE_PREV = 128 CMD_SUPPORT_QUEUE_SHUFFLE = 256 CMD_SUPPORT_QUEUE_REPEAT_ALL = 1024 CMD_SUPPORT_QUEUE_REPEAT_ONE = 2048 CMD_SUPPORT_QUEUE_REPEAT = 3072 CMD_SUPPORT_SKIP_AD = 512 CMD_SUPPORT_EDIT_TRACKS = 4096 CMD_SUPPORT_PLAYBACK_RATE = 8192 CMD_SUPPORT_LIKE = 16384 CMD_SUPPORT_DISLIKE = 32768 CMD_SUPPORT_FOLLOW = 65536 CMD_SUPPORT_UNFOLLOW = 131072 CMD_SUPPORT_STREAM_TRANSFER = 262144 # Legacy? CMD_SUPPORT_SKIP_FORWARD = 16 CMD_SUPPORT_SKIP_BACKWARD = 32 MediaImage = namedtuple("MediaImage", "url height width") _LOGGER = logging.getLogger(__name__) class MediaStatus: """ Class to hold the media status. """ # pylint: disable=too-many-instance-attributes,too-many-public-methods def __init__(self): self.current_time = 0 self.content_id = None self.content_type = None self.duration = None self.stream_type = STREAM_TYPE_UNKNOWN self.idle_reason = None self.media_session_id = None self.playback_rate = 1 self.player_state = MEDIA_PLAYER_STATE_UNKNOWN self.supported_media_commands = 0 self.volume_level = 1 self.volume_muted = False self.media_custom_data = {} self.media_metadata = {} self.subtitle_tracks = {} self.current_subtitle_tracks = [] self.last_updated = None @property def adjusted_current_time(self): """ Returns calculated current seek time of media in seconds """ if self.player_state == MEDIA_PLAYER_STATE_PLAYING: # Add time since last update return ( self.current_time + (datetime.utcnow() - self.last_updated).total_seconds() ) # Not playing, return last reported seek time return self.current_time @property def metadata_type(self): """ Type of meta data. """ return self.media_metadata.get("metadataType") @property def player_is_playing(self): """ Return True if player is PLAYING. """ return ( self.player_state == MEDIA_PLAYER_STATE_PLAYING or self.player_state == MEDIA_PLAYER_STATE_BUFFERING ) @property def player_is_paused(self): """ Return True if player is PAUSED. """ return self.player_state == MEDIA_PLAYER_STATE_PAUSED @property def player_is_idle(self): """ Return True if player is IDLE. """ return self.player_state == MEDIA_PLAYER_STATE_IDLE @property def media_is_generic(self): """ Return True if media status represents generic media. """ return self.metadata_type == METADATA_TYPE_GENERIC @property def media_is_tvshow(self): """ Return True if media status represents a tv show. """ return self.metadata_type == METADATA_TYPE_TVSHOW @property def media_is_movie(self): """ Return True if media status represents a movie. """ return self.metadata_type == METADATA_TYPE_MOVIE @property def media_is_musictrack(self): """ Return True if media status represents a musictrack. """ return self.metadata_type == METADATA_TYPE_MUSICTRACK @property def media_is_photo(self): """ Return True if media status represents a photo. """ return self.metadata_type == METADATA_TYPE_PHOTO @property def stream_type_is_buffered(self): """ Return True if stream type is BUFFERED. """ return self.stream_type == STREAM_TYPE_BUFFERED @property def stream_type_is_live(self): """ Return True if stream type is LIVE. """ return self.stream_type == STREAM_TYPE_LIVE @property def title(self): """ Return title of media. """ return self.media_metadata.get("title") @property def series_title(self): """ Return series title if available. """ return self.media_metadata.get("seriesTitle") @property def season(self): """ Return season if available. """ return self.media_metadata.get("season") @property def episode(self): """ Return episode if available. """ return self.media_metadata.get("episode") @property def artist(self): """ Return artist if available. """ return self.media_metadata.get("artist") @property def album_name(self): """ Return album name if available. """ return self.media_metadata.get("albumName") @property def album_artist(self): """ Return album artist if available. """ return self.media_metadata.get("albumArtist") @property def track(self): """ Return track number if available. """ return self.media_metadata.get("track") @property def images(self): """ Return a list of MediaImage objects for this media. """ return [ MediaImage(item.get("url"), item.get("height"), item.get("width")) for item in self.media_metadata.get("images", []) ] @property def supports_pause(self): """ True if PAUSE is supported. """ return bool(self.supported_media_commands & CMD_SUPPORT_PAUSE) @property def supports_seek(self): """ True if SEEK is supported. """ return bool(self.supported_media_commands & CMD_SUPPORT_SEEK) @property def supports_stream_volume(self): """ True if STREAM_VOLUME is supported. """ return bool(self.supported_media_commands & CMD_SUPPORT_STREAM_VOLUME) @property def supports_stream_mute(self): """ True if STREAM_MUTE is supported. """ return bool(self.supported_media_commands & CMD_SUPPORT_STREAM_MUTE) @property def supports_skip_forward(self): """ True if SKIP_FORWARD is supported. """ return bool(self.supported_media_commands & CMD_SUPPORT_SKIP_FORWARD) @property def supports_skip_backward(self): """ True if SKIP_BACKWARD is supported. """ return bool(self.supported_media_commands & CMD_SUPPORT_SKIP_BACKWARD) @property def supports_queue_next(self): """ True if QUEUE_NEXT is supported. """ return bool(self.supported_media_commands & CMD_SUPPORT_QUEUE_NEXT) @property def supports_queue_prev(self): """ True if QUEUE_PREV is supported. """ return bool(self.supported_media_commands & CMD_SUPPORT_QUEUE_PREV) def update(self, data): """ New data will only contain the changed attributes. """ if not data.get("status", []): return status_data = data["status"][0] media_data = status_data.get("media") or {} volume_data = status_data.get("volume", {}) self.current_time = status_data.get("currentTime", self.current_time) self.content_id = media_data.get("contentId", self.content_id) self.content_type = media_data.get("contentType", self.content_type) self.duration = media_data.get("duration", self.duration) self.stream_type = media_data.get("streamType", self.stream_type) self.idle_reason = status_data.get("idleReason", self.idle_reason) self.media_session_id = status_data.get("mediaSessionId", self.media_session_id) self.playback_rate = status_data.get("playbackRate", self.playback_rate) self.player_state = status_data.get("playerState", self.player_state) self.supported_media_commands = status_data.get( "supportedMediaCommands", self.supported_media_commands ) self.volume_level = volume_data.get("level", self.volume_level) self.volume_muted = volume_data.get("muted", self.volume_muted) self.media_custom_data = media_data.get("customData", self.media_custom_data) self.media_metadata = media_data.get("metadata", self.media_metadata) self.subtitle_tracks = media_data.get("tracks", self.subtitle_tracks) self.current_subtitle_tracks = status_data.get( "activeTrackIds", self.current_subtitle_tracks ) self.last_updated = datetime.utcnow() def __repr__(self): info = { "metadata_type": self.metadata_type, "title": self.title, "series_title": self.series_title, "season": self.season, "episode": self.episode, "artist": self.artist, "album_name": self.album_name, "album_artist": self.album_artist, "track": self.track, "subtitle_tracks": self.subtitle_tracks, "images": self.images, "supports_pause": self.supports_pause, "supports_seek": self.supports_seek, "supports_stream_volume": self.supports_stream_volume, "supports_stream_mute": self.supports_stream_mute, "supports_skip_forward": self.supports_skip_forward, "supports_skip_backward": self.supports_skip_backward, } info.update(self.__dict__) return "".format(info) # pylint: disable=too-many-public-methods class MediaController(BaseController): """ Controller to interact with Google media namespace. """ def __init__(self): super(MediaController, self).__init__("urn:x-cast:com.google.cast.media") self.media_session_id = 0 self.status = MediaStatus() self.session_active_event = threading.Event() self.app_id = APP_MEDIA_RECEIVER self._status_listeners = [] def channel_connected(self): """ Called when media channel is connected. Will update status. """ self.update_status() def channel_disconnected(self): """ Called when a media channel is disconnected. Will erase status. """ self.status = MediaStatus() self._fire_status_changed() def receive_message(self, message, data): """ Called when a media message is received. """ if data[MESSAGE_TYPE] == TYPE_MEDIA_STATUS: self._process_media_status(data) return True return False def register_status_listener(self, listener): """ Register a listener for new media statuses. A new status will call listener.new_media_status(status) """ self._status_listeners.append(listener) def update_status(self, callback_function_param=False): """ Send message to update the status. """ self.send_message( {MESSAGE_TYPE: TYPE_GET_STATUS}, callback_function=callback_function_param ) def _send_command(self, command): """ Send a command to the Chromecast on media channel. """ if self.status is None or self.status.media_session_id is None: self.logger.warning( "%s command requested but no session is active.", command[MESSAGE_TYPE] ) return command["mediaSessionId"] = self.status.media_session_id self.send_message(command, inc_session_id=True) @property def is_playing(self): """ Deprecated as of June 8, 2015. Use self.status.player_is_playing. Returns if the Chromecast is playing. """ return self.status is not None and self.status.player_is_playing @property def is_paused(self): """ Deprecated as of June 8, 2015. Use self.status.player_is_paused. Returns if the Chromecast is paused. """ return self.status is not None and self.status.player_is_paused @property def is_idle(self): """ Deprecated as of June 8, 2015. Use self.status.player_is_idle. Returns if the Chromecast is idle on a media supported app. """ return self.status is not None and self.status.player_is_idle @property def title(self): """ Deprecated as of June 8, 2015. Use self.status.title. Return title of the current playing item. """ return None if not self.status else self.status.title @property def thumbnail(self): """ Deprecated as of June 8, 2015. Use self.status.images. Return thumbnail url of current playing item. """ if not self.status: return None images = self.status.images return images[0].url if images else None def play(self): """ Send the PLAY command. """ self._send_command({MESSAGE_TYPE: TYPE_PLAY}) def pause(self): """ Send the PAUSE command. """ self._send_command({MESSAGE_TYPE: TYPE_PAUSE}) def stop(self): """ Send the STOP command. """ self._send_command({MESSAGE_TYPE: TYPE_STOP}) def rewind(self): """ Starts playing the media from the beginning. """ self.seek(0) def skip(self): """ Skips rest of the media. Values less then -5 behaved flaky. """ self.seek(int(self.status.duration) - 5) def seek(self, position): """ Seek the media to a specific location. """ self._send_command( { MESSAGE_TYPE: TYPE_SEEK, "currentTime": position, "resumeState": "PLAYBACK_START", } ) def queue_next(self): """ Send the QUEUE_NEXT command. """ self._send_command({MESSAGE_TYPE: TYPE_QUEUE_NEXT}) def queue_prev(self): """ Send the QUEUE_PREV command. """ self._send_command({MESSAGE_TYPE: TYPE_QUEUE_PREV}) def enable_subtitle(self, track_id): """ Enable specific text track. """ self._send_command( {MESSAGE_TYPE: TYPE_EDIT_TRACKS_INFO, "activeTrackIds": [track_id]} ) def disable_subtitle(self): """ Disable subtitle. """ self._send_command({MESSAGE_TYPE: TYPE_EDIT_TRACKS_INFO, "activeTrackIds": []}) def block_until_active(self, timeout=None): """ Blocks thread until the media controller session is active on the chromecast. The media controller only accepts playback control commands when a media session is active. If a session is already active then the method returns immediately. :param timeout: a floating point number specifying a timeout for the operation in seconds (or fractions thereof). Or None to block forever. """ self.session_active_event.wait(timeout=timeout) def _process_media_status(self, data): """ Processes a STATUS message. """ self.status.update(data) self.logger.debug("Media:Received status %s", data) # Update session active threading event if self.status.media_session_id is None: self.session_active_event.clear() else: self.session_active_event.set() self._fire_status_changed() def _fire_status_changed(self): """ Tells listeners of a changed status. """ for listener in self._status_listeners: try: listener.new_media_status(self.status) except Exception: # pylint: disable=broad-except _LOGGER.exception( "Exception thrown when calling media status " "callback" ) # pylint: disable=too-many-arguments def play_media( self, url, content_type, title=None, thumb=None, current_time=0, autoplay=True, stream_type=STREAM_TYPE_BUFFERED, metadata=None, subtitles=None, subtitles_lang="en-US", subtitles_mime="text/vtt", subtitle_id=1, ): """ Plays media on the Chromecast. Start default media receiver if not already started. Parameters: url: str - url of the media. content_type: str - mime type. Example: 'video/mp4'. title: str - title of the media. thumb: str - thumbnail image url. current_time: float - seconds from the beginning of the media to start playback. autoplay: bool - whether the media will automatically play. stream_type: str - describes the type of media artifact as one of the following: "NONE", "BUFFERED", "LIVE". subtitles: str - url of subtitle file to be shown on chromecast. subtitles_lang: str - language for subtitles. subtitles_mime: str - mimetype of subtitles. subtitle_id: int - id of subtitle to be loaded. metadata: dict - media metadata object, one of the following: GenericMediaMetadata, MovieMediaMetadata, TvShowMediaMetadata, MusicTrackMediaMetadata, PhotoMediaMetadata. Docs: https://developers.google.com/cast/docs/reference/messages#MediaData """ # pylint: disable=too-many-locals def app_launched_callback(): """Plays media after chromecast has switched to requested app.""" self._send_start_play_media( url, content_type, title, thumb, current_time, autoplay, stream_type, metadata, subtitles, subtitles_lang, subtitles_mime, subtitle_id, ) receiver_ctrl = self._socket_client.receiver_controller receiver_ctrl.launch_app(self.app_id, callback_function=app_launched_callback) def _send_start_play_media( self, url, content_type, title=None, thumb=None, current_time=0, autoplay=True, stream_type=STREAM_TYPE_BUFFERED, metadata=None, subtitles=None, subtitles_lang="en-US", subtitles_mime="text/vtt", subtitle_id=1, ): # pylint: disable=too-many-locals msg = { "media": { "contentId": url, "streamType": stream_type, "contentType": content_type, "metadata": metadata or {}, }, MESSAGE_TYPE: TYPE_LOAD, "currentTime": current_time, "autoplay": autoplay, "customData": {}, } if title: msg["media"]["metadata"]["title"] = title if thumb: msg["media"]["metadata"]["thumb"] = thumb if "images" not in msg["media"]["metadata"]: msg["media"]["metadata"]["images"] = [] msg["media"]["metadata"]["images"].append({"url": thumb}) if subtitles: sub_msg = [ { "trackId": subtitle_id, "trackContentId": subtitles, "language": subtitles_lang, "subtype": "SUBTITLES", "type": "TEXT", "trackContentType": subtitles_mime, "name": "{} - {} Subtitle".format(subtitles_lang, subtitle_id), } ] msg["media"]["tracks"] = sub_msg msg["media"]["textTrackStyle"] = { "backgroundColor": "#FFFFFF00", "edgeType": "OUTLINE", "edgeColor": "#000000FF", } msg["activeTrackIds"] = [subtitle_id] self.send_message(msg, inc_session_id=True) def tear_down(self): """ Called when controller is destroyed. """ super(MediaController, self).tear_down() self._status_listeners[:] = [] pychromecast-4.1.0/pychromecast/controllers/multizone.py000066400000000000000000000233151355177132200236760ustar00rootroot00000000000000""" Controller to monitor audio group members. """ import logging from . import BaseController from ..socket_client import ( CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED, CONNECTION_STATUS_LOST, ) _LOGGER = logging.getLogger(__name__) MESSAGE_TYPE = "type" MULTIZONE_NAMESPACE = "urn:x-cast:com.google.cast.multizone" TYPE_CASTING_GROUPS = "CASTING_GROUPS" TYPE_DEVICE_ADDED = "DEVICE_ADDED" TYPE_DEVICE_UPDATED = "DEVICE_UPDATED" TYPE_DEVICE_REMOVED = "DEVICE_REMOVED" TYPE_GET_CASTING_GROUPS = "GET_CASTING_GROUPS" TYPE_GET_STATUS = "GET_STATUS" TYPE_MULTIZONE_STATUS = "MULTIZONE_STATUS" TYPE_SESSION_UPDATED = "PLAYBACK_SESSION_UPDATED" class Listener: """ Callback handler. """ def __init__(self, group_cast, casts): """Initialize the listener.""" self._casts = casts group_cast.register_status_listener(self) group_cast.media_controller.register_status_listener(self) group_cast.register_connection_listener(self) self._mz = MultizoneController(group_cast.uuid) self._mz.register_listener(self) self._group_uuid = str(group_cast.uuid) group_cast.register_handler(self._mz) def new_cast_status(self, cast_status): """Handle reception of a new CastStatus.""" casts = self._casts group_members = self._mz.members for member_uuid in group_members: if member_uuid not in casts: continue for listener in list(casts[member_uuid]["listeners"]): listener.multizone_new_cast_status(self._group_uuid, cast_status) def new_media_status(self, media_status): """Handle reception of a new MediaStatus.""" casts = self._casts group_members = self._mz.members for member_uuid in group_members: if member_uuid not in casts: continue for listener in list(casts[member_uuid]["listeners"]): listener.multizone_new_media_status(self._group_uuid, media_status) def new_connection_status(self, conn_status): """Handle reception of a new ConnectionStatus.""" if conn_status.status == CONNECTION_STATUS_CONNECTED: self._mz.update_members() if ( conn_status.status == CONNECTION_STATUS_DISCONNECTED or conn_status.status == CONNECTION_STATUS_LOST ): self._mz.reset_members() def multizone_member_added(self, member_uuid): """Handle added audio group member.""" casts = self._casts if member_uuid not in casts: casts[member_uuid] = {"listeners": [], "groups": set()} casts[member_uuid]["groups"].add(self._group_uuid) for listener in list(casts[member_uuid]["listeners"]): listener.added_to_multizone(self._group_uuid) def multizone_member_removed(self, member_uuid): """Handle removed audio group member.""" casts = self._casts if member_uuid not in casts: casts[member_uuid] = {"listeners": [], "groups": set()} casts[member_uuid]["groups"].discard(self._group_uuid) for listener in list(casts[member_uuid]["listeners"]): listener.removed_from_multizone(self._group_uuid) def multizone_status_received(self): """Handle reception of audio group status.""" class MultizoneManager: """ Manage audio groups. """ def __init__(self): # Protect self._casts because it will be accessed from callbacks from # the casts' socket_client thread self._casts = {} self._groups = {} def add_multizone(self, group_cast): """ Start managing a group """ self._groups[str(group_cast.uuid)] = { "chromecast": group_cast, "listener": Listener(group_cast, self._casts), "members": set(), } def remove_multizone(self, group_uuid): """ Stop managing a group """ group_uuid = str(group_uuid) group = self._groups.pop(group_uuid, None) # Inform all group members that they are no longer members if group is not None: group["listener"]._mz.reset_members() # pylint: disable=protected-access for member in self._casts.values(): member["groups"].discard(group_uuid) def register_listener(self, member_uuid, listener): """ Register a listener for audio group changes of cast uuid. On update will call: listener.added_to_multizone(group_uuid) The cast has been added to group uuid listener.removed_from_multizone(group_uuid) The cast has been removed from group uuid listener.multizone_new_media_status(group_uuid, media_status) The group uuid, of which the cast is a member, has new status listener.multizone_new_cast_status(group_uuid, cast_status) The group uuid, of which the cast is a member, has new status """ member_uuid = str(member_uuid) if member_uuid not in self._casts: self._casts[member_uuid] = {"listeners": [], "groups": set()} self._casts[member_uuid]["listeners"].append(listener) def deregister_listener(self, member_uuid, listener): """ Deregister listener for audio group changes of cast uuid.""" self._casts[str(member_uuid)]["listeners"].remove(listener) def get_multizone_memberships(self, member_uuid): """ Return a list of audio groups in which cast member_uuid is a member """ return list(self._casts[str(member_uuid)]["groups"]) def get_multizone_mediacontroller(self, group_uuid): """ Get mediacontroller of a group """ return self._groups[str(group_uuid)]["chromecast"].media_controller class MultizoneController(BaseController): """ Controller to monitor audio group members. """ def __init__(self, uuid): self._members = {} self._status_listeners = [] self._uuid = str(uuid) super(MultizoneController, self).__init__( MULTIZONE_NAMESPACE, target_platform=True ) def _add_member(self, uuid, name): if uuid not in self._members: self._members[uuid] = name _LOGGER.debug( "(%s) Added member %s(%s), members: %s", self._uuid, uuid, name, self._members, ) for listener in list(self._status_listeners): listener.multizone_member_added(uuid) def _remove_member(self, uuid): name = self._members.pop(uuid, "") _LOGGER.debug( "(%s) Removed member %s(%s), members: %s", self._uuid, uuid, name, self._members, ) for listener in list(self._status_listeners): listener.multizone_member_removed(uuid) def register_listener(self, listener): """ Register a listener for audio group changes. On update will call: listener.multizone_member_added(uuid) listener.multizone_member_removed(uuid) listener.multizone_status_received() """ self._status_listeners.append(listener) @property def members(self): """ Return a list of audio group members. """ return list(self._members.keys()) def reset_members(self): """ Reset audio group members. """ for uuid in list(self._members): self._remove_member(uuid) def update_members(self): """ Update audio group members. """ self.send_message({MESSAGE_TYPE: TYPE_GET_STATUS}) def get_casting_groups(self): """ Send GET_CASTING_GROUPS message. """ self.send_message({MESSAGE_TYPE: TYPE_GET_CASTING_GROUPS}) def receive_message( self, message, data ): # noqa: E501 pylint: disable=too-many-return-statements """ Called when a multizone message is received. """ if data[MESSAGE_TYPE] == TYPE_DEVICE_ADDED: uuid = data["device"]["deviceId"] name = data["device"]["name"] self._add_member(uuid, name) return True if data[MESSAGE_TYPE] == TYPE_DEVICE_REMOVED: uuid = data["deviceId"] self._remove_member(uuid) return True if data[MESSAGE_TYPE] == TYPE_DEVICE_UPDATED: uuid = data["device"]["deviceId"] name = data["device"]["name"] self._add_member(uuid, name) return True if data[MESSAGE_TYPE] == TYPE_MULTIZONE_STATUS: members = data["status"]["devices"] members = {member["deviceId"]: member["name"] for member in members} removed_members = list(set(self._members.keys()) - set(members.keys())) added_members = list(set(members.keys()) - set(self._members.keys())) _LOGGER.debug( "(%s) Added members %s, Removed members: %s", self._uuid, added_members, removed_members, ) for uuid in removed_members: self._remove_member(uuid) for uuid in added_members: self._add_member(uuid, members[uuid]) for listener in list(self._status_listeners): listener.multizone_status_received() return True if data[MESSAGE_TYPE] == TYPE_SESSION_UPDATED: # A temporary group has been formed return True if data[MESSAGE_TYPE] == TYPE_CASTING_GROUPS: # Answer to GET_CASTING_GROUPS return True return False def tear_down(self): """ Called when controller is destroyed. """ super(MultizoneController, self).tear_down() self._status_listeners[:] = [] pychromecast-4.1.0/pychromecast/controllers/plex.py000066400000000000000000000424051355177132200226210ustar00rootroot00000000000000""" Controller to interface with the Plex-app. """ import json import threading from copy import deepcopy from urllib.parse import urlparse from . import BaseController MESSAGE_TYPE = "type" STREAM_TYPE_UNKNOWN = "UNKNOWN" STREAM_TYPE_BUFFERED = "BUFFERED" STREAM_TYPE_LIVE = "LIVE" SEEK_KEY = "currentTime" TYPE_PLAY = "PLAY" TYPE_PAUSE = "PAUSE" TYPE_STOP = "STOP" TYPE_STEPFORWARD = "STEPFORWARD" TYPE_STEPBACKWARD = "STEPBACK" TYPE_PREVIOUS = "PREVIOUS" TYPE_NEXT = "NEXT" TYPE_LOAD = "LOAD" TYPE_DETAILS = "SHOWDETAILS" TYPE_SEEK = "SEEK" TYPE_MEDIA_STATUS = "MEDIA_STATUS" TYPE_GET_STATUS = "GET_STATUS" TYPE_EDIT_TRACKS_INFO = "EDIT_TRACKS_INFO" def media_to_chromecast_command( media=None, type="LOAD", requestId=1, offset=0, directPlay=True, directStream=True, subtitleSize=100, audioBoost=100, transcoderVideo=True, transcoderVideoRemuxOnly=False, transcoderAudio=True, isVerifiedHostname=True, contentType=("video/mp4"), myPlexSubscription=True, contentId=None, streamType=STREAM_TYPE_BUFFERED, port=32400, protocol="http", address=None, username=None, autoplay=True, currentTime=0, playQueueID=None, **kwargs ): # noqa: 501 pylint: disable=invalid-name, too-many-arguments, too-many-locals, protected-access, redefined-builtin """Create the message that chromecast requires. Use pass of plexapi media object or set all the neeeded kwargs manually. See the code for what to set. Args: media (None, optional): a :class:`~plexapi.base.Playable type (str): default LOAD other possible is SHOWDETAILS requestId (int): The requestId, think chromecast uses this. offset (int): Offset of the playback in seconds. directPlay (bool): Default True directStream (bool): Default True subtitleSize (int): Set the subtitle size, only seen 100 and 200 so far. audioBoost (int): Default 100 transcoderVideo (bool): Default True transcoderVideoRemuxOnly (bool): Default False transcoderAudio (bool): Default True isVerifiedHostname (bool): Default True contentType (str): default ('video/mp4'), ('audio/mp3') if audio myPlexSubscription (bool): Has the user a plexpass contentId (str): They key chromecast use to start playback. streamType (str): Default BUFFERED, LIVE port (int): pms port address (str): pms host, without scheme username (None): user name of the person that start the playback. autoplay (bool): Auto play after the video is done. currentTime (int): Set playback from this time. default 0 **kwargs: To allow overrides, this will be merged with the rest of the msg. Returns: dict: Returs a dict formatted correctly to start playback on a chromecast. """ # noqa if media is not None: # Let set som params for the user if they use plexapi. server_url = urlparse(media._server._baseurl) contentType = ( ("video/mp4") if media.TYPE in ("movie", "episode") else ("audio/mp3") ) protocol = server_url.scheme address = server_url.hostname port = server_url.port machineIdentifier = media._server.machineIdentifier playQueueID = media._server.createPlayQueue(media).playQueueID token = media._server._token username = media._server.myPlexUsername myPlexSubscription = media._server.myPlexSubscription contentId = media.key # Lets see if this helps # chrome cast seems to start playback # 5 sec before the offset. if offset != 0: currentTime = offset msg = { "type": type, "requestId": requestId, "media": { "contentId": contentId, "streamType": streamType, "contentType": contentType, "customData": { "offset": offset, "directPlay": directPlay, "directStream": directStream, "subtitleSize": subtitleSize, "audioBoost": audioBoost, "server": { "machineIdentifier": machineIdentifier, "transcoderVideo": transcoderVideo, "transcoderVideoRemuxOnly": transcoderVideoRemuxOnly, "transcoderAudio": transcoderAudio, "version": "1.4.3.3433", "myPlexSubscription": myPlexSubscription, "isVerifiedHostname": isVerifiedHostname, "protocol": protocol, "address": address, "port": port, "accessToken": token, "user": {"username": username}, }, "containerKey": "/playQueues/%s?own=1&window=200" % playQueueID, # noqa: E501 }, "autoplay": autoplay, "currentTime": currentTime, "activeTrackIds": None, }, } # Allow passing kwarg to the dict msg.update(kwargs) return msg @property def episode_title(self): """Return episode title.""" return self.media_metadata.get("subtitle") class PlexController(BaseController): # pylint: disable=too-many-public-methods """ Controller to interact with Plex namespace. """ def __init__(self): super(PlexController, self).__init__("urn:x-cast:plex", "9AC194DC") self.app_id = "9AC194DC" self.namespace = "urn:x-cast:plex" self.request_id = 0 self.play_media_event = threading.Event() self._last_play_msg = {} def _send_cmd( self, msg, namespace=None, inc_session_id=False, callback_function=None, inc=True, ): # pylint: disable=too-many-arguments """Wrapper the commands. Args: msg (dict): the actual command that will be sent. namespace (None, optional): What namespace should se use to send this. inc_session_id (bool, optional): Include session id. callback_function (None, optional): If given the callback is exceuted after the command is executed. inc (bool, optional): Increase the requestsId. """ # noqa self.logger.debug( "Sending msg %r %s %s %s %s", msg, namespace, inc_session_id, callback_function, inc, ) if inc: self._inc_request() if namespace: old = self.namespace try: self.namespace = namespace self.send_message( msg, inc_session_id=inc_session_id, callback_function=callback_function, ) finally: self.namespace = old else: self.send_message( msg, inc_session_id=inc_session_id, callback_function=callback_function ) def _inc_request(self): # is this needed? dunno if this is getting passed to plex self.request_id += 1 return self.request_id def channel_connected(self): """Called when media channel is connected. Will update status.""" self.update_status() def receive_message(self, message, data): """Called when a messag from plex to our controller is received. I havnt seen any message for ut but lets keep for for now, the tests i have done is minimal. Args: message (dict): Description data (dict): Description Returns: bool: True if the message is handled, False if not. """ if data[MESSAGE_TYPE] == TYPE_MEDIA_STATUS: self.logger.debug("(PlexController) MESSAGE RECEIVED: %r", data) return True return False def update_status(self, callback_function_param=False): """Send message to update the status.""" self.send_message( {MESSAGE_TYPE: TYPE_GET_STATUS}, callback_function=callback_function_param ) def stop(self): """Send stop command.""" self._send_cmd({MESSAGE_TYPE: TYPE_STOP}) def pause(self): """Send pause command.""" self._send_cmd({MESSAGE_TYPE: TYPE_PAUSE}) def play(self): """Send play command.""" self._send_cmd({MESSAGE_TYPE: TYPE_PLAY}) def previous(self): """Send previous command.""" self._send_cmd({MESSAGE_TYPE: TYPE_PREVIOUS}) def next(self): """Send next command.""" self._send_cmd({MESSAGE_TYPE: TYPE_NEXT}) def seek(self, position, resume_state="PLAYBACK_START"): """Send seek command Args: position (int): offset in seconds. resume_state (str, default): PLAYBACK_START """ self._send_cmd( {MESSAGE_TYPE: TYPE_SEEK, SEEK_KEY: position, "resumeState": resume_state} ) def rewind(self): """Rewind back to the start""" self.seek(0) def set_volume(self, percent): """Set the volume 1-100 Args: percent (int): The wanted volume. """ self._socket_client.receiver_controller.set_volume( float(percent / 100) ) # noqa: 501 def volume_up(self, delta=0.1): """ Increment volume by 0.1 (or delta) unless it is already maxed. Returns the new volume. """ if delta <= 0: raise ValueError( "volume delta must be greater than zero, not {}".format(delta) ) return self.set_volume(self.status.volume_level + delta) def volume_down(self, delta=0.1): """ Decrement the volume by 0.1 (or delta) unless it is already 0. Returns the new volume. """ if delta <= 0: raise ValueError( "volume delta must be greater than zero, not {}".format(delta) ) return self.set_volume(self.status.volume_level - delta) def mute(self, status=None): """mute the sound, acts as on off. Args: status (None, optional): override for on/off """ if status is not None: status = status else: status = not status.volume_muted self._socket_client.receiver_controller.set_volume_muted(status) def show_media(self, media=None, **kwargs): """Show the media on the screen""" msg = media_to_chromecast_command( media, type=TYPE_DETAILS, requestId=self._inc_request(), **kwargs ) def callback(): # pylint: disable=missing-docstring self._send_cmd(msg, inc_session_id=True, inc=False) self.launch(callback) def quit_app(self): """Quit the plex app""" self._socket_client.receiver_controller.stop_app() @property def status(self): """Get the chromecast playing status. Returns: pychromecast.controllers.media.MediaStatus: Slightly modified status with patched method for episode_title. """ # noqa status = self._socket_client.media_controller.status status.episode_title = episode_title return status def _reset_playback(self, offset=None): """Reset playback. Args: offset (None, optional): What time should the stream start again, if omitted the platback will start from current time. Setting it will override this behaviour. This is given in seconds. """ # noqa if self._last_play_msg: offset_now = self.status.adjusted_current_time msg = deepcopy(self._last_play_msg) if offset is None: msg["media"]["customData"]["offset"] = offset_now msg["current_time"] = offset_now else: msg["media"]["customData"]["offset"] = offset msg["current_time"] = offset_now self._send_cmd( msg, namespace="urn:x-cast:com.google.cast.media", inc_session_id=True, inc=False, ) else: self.logger.debug( "Cant reset the stream as _last_play_msg " "is not set by _send_start_play" ) def _send_start_play(self, media=None, **kwargs): """Helper to send a playback command. Args: media (None, optional): :class:`~plexapi.base.Playable **kwargs: media_to_chromecast_command docs string. """ msg = media_to_chromecast_command( media, requestiId=self._inc_request(), **kwargs ) self.logger.debug("Create command: \n%r\n", json.dumps(msg, indent=4)) self._last_play_msg = msg self._send_cmd( msg, namespace="urn:x-cast:com.google.cast.media", inc_session_id=True, inc=False, ) def block_until_playing(self, media=None, timeout=None, **kwargs): """Block until this playing, typically usefull in a script another way to the the same is the check if the controllers is_active or use self.status.player_state Args: media (None, optional): Can also be :class:`~plexapi.base.Playable if its not, you need to fill out all the kwargs. timeout (None, int): default None **kwargs: See media_to_chromecast_command docs string. """ # noqa # Incase media isnt playing self.play_media_event.clear() self.play_media(media, **kwargs) self.play_media_event.wait(timeout) self.play_media_event.clear() def play_media(self, media=None, **kwargs): """Start playback on the chromecast Args: media (None, optional): Can also be :class:`~plexapi.base.Playable if its not, you need to fill out all the kwargs. **kwargs: See media_to_chromecast_command docs string. """ # noqa self.play_media_event.clear() def app_launched_callback(): # pylint: disable=missing-docstring try: self._send_start_play(media, **kwargs) finally: self.play_media_event.set() self.launch(app_launched_callback) def join(self, timeout=None): """Join the thread.""" self._socket_client.join(timeout=timeout) def disconnect(self, timeout=None, blocking=True): """Disconnect the controller.""" self._socket_client.disconnect() if blocking: self.join(timeout=timeout) # pylint: disable=too-many-public-methods class PlexApiController(PlexController): """A controller that can use plexapi..""" def __init__(self, pms): super(PlexApiController, self).__init__() self.pms = pms def _get_current_media(self): """Get current media_item, media and part for pms.""" key = int(self.status.content_id.split("/")[-1]) media_item = self.pms.fetchItem(key).reload() media_idx = self.status.media_custom_data.get("mediaIndex", 0) part_idx = self.status.media_custom_data.get("partIndex", 0) media = media_item.media[media_idx] part = media.parts[part_idx] return media_item, media, part def _change_track(self, track, type_="subtitle", reset_playback=True): """Sets a new default audio/subtitle track so mde select the correct track. Args: track (None): what track we should choose. type_ (str): what type of track reset_playback (bool, optional): Reset the playback after the track has been changed. Raises: ValueError: If type isn't subtitle or audio. """ # noqa item, _, part = self._get_current_media() if type_ == "subtitle": method = part.subtitleStreams() default = part.setDefaultSubtitleStream elif type_ == "audio": method = part.audioStreams() default = part.setDefaultAudioStream else: raise ValueError("set type parmenter as subtitle or audio") for track_ in method: if track in (track_.index, track_.language, track_.languageCode): self.logger.debug("Change %s to %s", type_, track) default(track_) break item.reload() if reset_playback: self._reset_playback() def enable_audiotrack(self, audio): """Enable a audiotrack. Args: audio (str): could be index, language or languageCode. """ self._change_track(self, audio, "audio") def disable_subtitle(self): """Disable a subtitle.""" _, __, part = ( self._get_current_media() ) # noqa: 501 pylint disable=unused-variable part.resetDefaultSubtitleStream() self._reset_playback() def enable_subtitle(self, subtitle): """Enable a subtitle track. Args: subtitle (str): could be index, language or languageCode. """ self._change_track(subtitle) pychromecast-4.1.0/pychromecast/controllers/spotify.py000066400000000000000000000055371355177132200233530ustar00rootroot00000000000000""" Controller to interface with Spotify. """ import logging import threading from . import BaseController from ..config import APP_SPOTIFY from ..error import LaunchError APP_NAMESPACE = "urn:x-cast:com.spotify.chromecast.secure.v1" TYPE_GET_INFO = "getInfo" TYPE_GET_INFO_RESPONSE = "getInfoResponse" TYPE_SET_CREDENTIALS = "setCredentials" TYPE_SET_CREDENTIALS_ERROR = "setCredentialsError" TYPE_SET_CREDENTIALS_RESPONSE = "setCredentialsResponse" # pylint: disable=too-many-instance-attributes class SpotifyController(BaseController): """ Controller to interact with Spotify namespace. """ # pylint: disable=useless-super-delegation # The pylint rule useless-super-delegation doesn't realize # we are setting default values here. def __init__(self, access_token, expires): super(SpotifyController, self).__init__(APP_NAMESPACE, APP_SPOTIFY) if access_token is None or expires is None: raise ValueError("access_token and expires cannot be empty") self.logger = logging.getLogger(__name__) self.session_started = False self.access_token = access_token self.expires = expires self.is_launched = False self.device = None self.credential_error = False self.waiting = threading.Event() # pylint: enable=useless-super-delegation # pylint: disable=unused-argument,no-self-use def receive_message(self, message, data): """ Handle the auth flow and active player selection """ if data["type"] == TYPE_SET_CREDENTIALS_RESPONSE: self.send_message({"type": TYPE_GET_INFO, "payload": {}}) if data["type"] == TYPE_SET_CREDENTIALS_ERROR: self.device = None self.credential_error = True self.waiting.set() if data["type"] == TYPE_GET_INFO_RESPONSE: self.device = data["payload"]["deviceID"] self.is_launched = True self.waiting.set() return True def launch_app(self, timeout=10): """ Launch Spotify application. Will raise a LaunchError exception if there is no response from the Spotify app within timeout seconds. """ def callback(): """Callback function""" self.send_message( { "type": TYPE_SET_CREDENTIALS, "credentials": self.access_token, "expiresIn": self.expires, } ) self.device = None self.credential_error = False self.waiting.clear() self.launch(callback_function=callback) # Need to wait for Spotify to be launched on Chromecast completely self.waiting.wait(timeout) if not self.is_launched: raise LaunchError( "Timeout when waiting for status response from Spotify app" ) pychromecast-4.1.0/pychromecast/controllers/youtube.py000066400000000000000000000066151355177132200233500ustar00rootroot00000000000000""" Controller to interface with the YouTube-app. Use the media controller to play, pause etc. """ import threading from casttube import YouTubeSession from . import BaseController from ..error import UnsupportedNamespace from ..config import APP_YOUTUBE YOUTUBE_NAMESPACE = "urn:x-cast:com.google.youtube.mdx" TYPE_GET_SCREEN_ID = "getMdxSessionStatus" TYPE_STATUS = "mdxSessionStatus" ATTR_SCREEN_ID = "screenId" MESSAGE_TYPE = "type" class YouTubeController(BaseController): """ Controller to interact with Youtube.""" def __init__(self): super(YouTubeController, self).__init__(YOUTUBE_NAMESPACE, APP_YOUTUBE) self.status_update_event = threading.Event() self._screen_id = None self._session = None def start_session_if_none(self): """ Starts a session it is not yet initialized. """ if not (self._screen_id and self._session): self.update_screen_id() self._session = YouTubeSession(screen_id=self._screen_id) def play_video(self, video_id, playlist_id=None): """ Play video(video_id) now. This ignores the current play queue order. :param video_id: YouTube video id(http://youtube.com/watch?v=video_id) :param playlist_id: youtube.com/watch?v=video_id&list=playlist_id """ self.start_session_if_none() self._session.play_video(video_id, playlist_id) def add_to_queue(self, video_id): """ Add video(video_id) to the end of the play queue. :param video_id: YouTube video id(http://youtube.com/watch?v=video_id) """ self.start_session_if_none() self._session.add_to_queue(video_id) def play_next(self, video_id): """ Play video(video_id) after the currently playing video. :param video_id: YouTube video id(http://youtube.com/watch?v=video_id) """ self.start_session_if_none() self._session.play_next(video_id) def remove_video(self, video_id): """ Remove video(videoId) from the queue. :param video_id: YouTube video id(http://youtube.com/watch?v=video_id) """ self.start_session_if_none() self._session.remove_video(video_id) def clear_playlist(self): """ Clear the entire video queue """ self.start_session_if_none() self._session.clear_playlist() def update_screen_id(self): """ Sends a getMdxSessionStatus to get the screenId and waits for response. This function is blocking If connected we should always get a response (send message will launch app if it is not running). """ self.status_update_event.clear() # This gets the screenId but always throws. Couldn't find a better way. try: self.send_message({MESSAGE_TYPE: TYPE_GET_SCREEN_ID}) except UnsupportedNamespace: pass self.status_update_event.wait() self.status_update_event.clear() def receive_message(self, message, data): """ Called when a media message is received. """ if data[MESSAGE_TYPE] == TYPE_STATUS: self._process_status(data.get("data")) return True return False def _process_status(self, status): """ Process latest status update. """ self._screen_id = status.get(ATTR_SCREEN_ID) self.status_update_event.set() pychromecast-4.1.0/pychromecast/dial.py000066400000000000000000000116151355177132200202130ustar00rootroot00000000000000""" Implements the DIAL-protocol to communicate with the Chromecast """ from collections import namedtuple from uuid import UUID import logging import requests from .discovery import get_info_from_service, get_host_from_service_info XML_NS_UPNP_DEVICE = "{urn:schemas-upnp-org:device-1-0}" FORMAT_BASE_URL = "http://{}:8008" # Regular chromecast, supports video/audio CAST_TYPE_CHROMECAST = "cast" # Cast Audio device, supports only audio CAST_TYPE_AUDIO = "audio" # Cast Audio group device, supports only audio CAST_TYPE_GROUP = "group" CAST_TYPES = { "chromecast": CAST_TYPE_CHROMECAST, "eureka dongle": CAST_TYPE_CHROMECAST, "chromecast audio": CAST_TYPE_AUDIO, "google home": CAST_TYPE_AUDIO, "google home mini": CAST_TYPE_AUDIO, "google cast group": CAST_TYPE_GROUP, } _LOGGER = logging.getLogger(__name__) def reboot(host): """ Reboots the chromecast. """ headers = {"content-type": "application/json"} requests.post( FORMAT_BASE_URL.format(host) + "/setup/reboot", data='{"params":"now"}', headers=headers, timeout=10, ) def _get_status(host, services, zconf, path): """ :param host: Hostname or ip to fetch status from :type host: str :return: The device status as a named tuple. :rtype: pychromecast.dial.DeviceStatus or None """ if not host: for service in services.copy(): service_info = get_info_from_service(service, zconf) host, _ = get_host_from_service_info(service_info) if host: _LOGGER.debug("Resolved service %s to %s", service, host) break headers = {"content-type": "application/json"} req = requests.get(FORMAT_BASE_URL.format(host) + path, headers=headers, timeout=10) req.raise_for_status() # The Requests library will fall back to guessing the encoding in case # no encoding is specified in the response headers - which is the case # for the Chromecast. # The standard mandates utf-8 encoding, let's fall back to that instead # if no encoding is provided, since the autodetection does not always # provide correct results. if req.encoding is None: req.encoding = "utf-8" return req.json() def get_device_status(host, services=None, zconf=None): """ :param host: Hostname or ip to fetch status from :type host: str :return: The device status as a named tuple. :rtype: pychromecast.dial.DeviceStatus or None """ try: status = _get_status(host, services, zconf, "/setup/eureka_info?options=detail") friendly_name = status.get("name", "Unknown Chromecast") model_name = "Unknown model name" manufacturer = "Unknown manufacturer" if "detail" in status: model_name = status["detail"].get("model_name", model_name) manufacturer = status["detail"].get("manufacturer", manufacturer) udn = status.get("ssdp_udn", None) cast_type = CAST_TYPES.get(model_name.lower(), CAST_TYPE_CHROMECAST) uuid = None if udn: uuid = UUID(udn.replace("-", "")) return DeviceStatus(friendly_name, model_name, manufacturer, uuid, cast_type) except (requests.exceptions.RequestException, OSError, ValueError): return None def get_multizone_status(host, services=None, zconf=None): """ :param host: Hostname or ip to fetch status from :type host: str :return: The multizone status as a named tuple. :rtype: pychromecast.dial.MultizoneStatus or None """ try: status = status = _get_status( host, services, zconf, "/setup/eureka_info?params=multizone" ) dynamic_groups = [] if "multizone" in status and "dynamic_groups" in status["multizone"]: for group in status["multizone"]["dynamic_groups"]: name = group.get("name", "Unknown group name") udn = group.get("uuid", None) uuid = None if udn: uuid = UUID(udn.replace("-", "")) dynamic_groups.append(MultizoneInfo(name, uuid)) groups = [] if "multizone" in status and "groups" in status["multizone"]: for group in status["multizone"]["groups"]: name = group.get("name", "Unknown group name") udn = group.get("uuid", None) uuid = None if udn: uuid = UUID(udn.replace("-", "")) groups.append(MultizoneInfo(name, uuid)) return MultizoneStatus(dynamic_groups, groups) except (requests.exceptions.RequestException, OSError, ValueError): return None DeviceStatus = namedtuple( "DeviceStatus", ["friendly_name", "model_name", "manufacturer", "uuid", "cast_type"] ) MultizoneInfo = namedtuple("MultizoneInfo", ["friendly_name", "uuid"]) MultizoneStatus = namedtuple("MultizoneStatus", ["dynamic_groups", "groups"]) pychromecast-4.1.0/pychromecast/discovery.py000066400000000000000000000124021355177132200213040ustar00rootroot00000000000000"""Discovers Chromecasts on the network using mDNS/zeroconf.""" import logging import socket from uuid import UUID import zeroconf DISCOVER_TIMEOUT = 5 _LOGGER = logging.getLogger(__name__) class CastListener: """Zeroconf Cast Services collection.""" def __init__(self, add_callback=None, remove_callback=None): self.services = {} self.add_callback = add_callback self.remove_callback = remove_callback @property def count(self): """Number of discovered cast services.""" return len(self.services) @property def devices(self): """List of tuples (ip, host) for each discovered device.""" return list(self.services.values()) # pylint: disable=unused-argument def remove_service(self, zconf, typ, name): """ Remove a service from the collection. """ _LOGGER.debug("remove_service %s, %s", typ, name) service = self.services.pop(name, None) if not service: _LOGGER.debug("remove_service unknown %s, %s", typ, name) return if self.remove_callback: self.remove_callback(name, service) def add_service(self, zconf, typ, name): """ Add a service to the collection. """ service = None tries = 0 _LOGGER.debug("add_service %s, %s", typ, name) while service is None and tries < 4: try: service = zconf.get_service_info(typ, name) except IOError: # If the zeroconf fails to receive the necessary data we abort # adding the service break tries += 1 if not service: _LOGGER.debug("add_service failed to add %s, %s", typ, name) return def get_value(key): """Retrieve value and decode to UTF-8.""" value = service.properties.get(key.encode("utf-8")) if value is None or isinstance(value, str): return value return value.decode("utf-8") ips = zconf.cache.entries_with_name(service.server.lower()) host = repr(ips[0]) if ips else service.server model_name = get_value("md") uuid = get_value("id") friendly_name = get_value("fn") if uuid: uuid = UUID(uuid) self.services[name] = (host, service.port, uuid, model_name, friendly_name) if self.add_callback: self.add_callback(name) def start_discovery(add_callback=None, remove_callback=None): """ Start discovering chromecasts on the network. This method will start discovering chromecasts on a separate thread. When a chromecast is discovered, the callback will be called with the discovered chromecast's zeroconf name. This is the dictionary key to find the chromecast metadata in listener.services. This method returns the CastListener object and the zeroconf ServiceBrowser object. The CastListener object will contain information for the discovered chromecasts. To stop discovery, call the stop_discovery method with the ServiceBrowser object. """ listener = CastListener(add_callback, remove_callback) service_browser = False try: service_browser = zeroconf.ServiceBrowser( zeroconf.Zeroconf(), "_googlecast._tcp.local.", listener ) except ( zeroconf.BadTypeInNameException, NotImplementedError, OSError, socket.error, zeroconf.NonUniqueNameException, ): pass return listener, service_browser def stop_discovery(browser): """Stop the chromecast discovery thread.""" browser.zc.close() def discover_chromecasts(max_devices=None, timeout=DISCOVER_TIMEOUT): """ Discover chromecasts on the network. """ from threading import Event browser = False try: # pylint: disable=unused-argument def callback(name): """Called when zeroconf has discovered a new chromecast.""" if max_devices is not None and listener.count >= max_devices: discover_complete.set() discover_complete = Event() listener, browser = start_discovery(callback) # Wait for the timeout or the maximum number of devices discover_complete.wait(timeout) return listener.devices finally: if browser is not False: stop_discovery(browser) def get_info_from_service(service, zconf): """ Resolve service_info from service. """ service_info = None try: service_info = zconf.get_service_info("_googlecast._tcp.local.", service) if service_info: _LOGGER.debug( "get_info_from_service resolved service %s to service_info %s", service, service_info, ) except IOError: pass return service_info def get_host_from_service_info(service_info): """ Get hostname or IP from service_info. """ host = None port = None if ( service_info and service_info.port and (service_info.server or service_info.address) ): if service_info.address: host = socket.inet_ntoa(service_info.address) else: host = service_info.server.lower() port = service_info.port return (host, port) pychromecast-4.1.0/pychromecast/error.py000066400000000000000000000023451355177132200204330ustar00rootroot00000000000000""" Errors to be used by PyChromecast. """ class PyChromecastError(Exception): """ Base error for PyChromecast. """ class NoChromecastFoundError(PyChromecastError): """ When a command has to auto-discover a Chromecast and cannot find one. """ class MultipleChromecastsFoundError(PyChromecastError): """ When getting a singular chromecast results in getting multiple chromecasts. """ class ChromecastConnectionError(PyChromecastError): """ When a connection error occurs within PyChromecast. """ class LaunchError(PyChromecastError): """ When an app fails to launch. """ class PyChromecastStopped(PyChromecastError): """ Raised when a command is invoked while the Chromecast's socket_client is stopped. """ class NotConnected(PyChromecastError): """ Raised when a command is invoked while not connected to a Chromecast. """ class UnsupportedNamespace(PyChromecastError): """ Raised when trying to send a message with a namespace that is not supported by the current running app. """ class ControllerNotRegistered(PyChromecastError): """ Raised when trying to interact with a controller while it is not registered with a ChromeCast object. """ pychromecast-4.1.0/pychromecast/logging_pb2.py000066400000000000000000001072371355177132200215010ustar00rootroot00000000000000# Generated by the protocol buffer compiler. DO NOT EDIT! # source: logging.proto import sys _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) from google.protobuf.internal import enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database from google.protobuf import descriptor_pb2 # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor.FileDescriptor( name='logging.proto', package='extensions.api.cast_channel.proto', syntax='proto2', serialized_pb=_b('\n\rlogging.proto\x12!extensions.api.cast_channel.proto\"\xfd\x04\n\x0bSocketEvent\x12:\n\x04type\x18\x01 \x01(\x0e\x32,.extensions.api.cast_channel.proto.EventType\x12\x18\n\x10timestamp_micros\x18\x02 \x01(\x03\x12\x0f\n\x07\x64\x65tails\x18\x03 \x01(\t\x12\x18\n\x10net_return_value\x18\x04 \x01(\x05\x12\x19\n\x11message_namespace\x18\x05 \x01(\t\x12\x42\n\x0bready_state\x18\x06 \x01(\x0e\x32-.extensions.api.cast_channel.proto.ReadyState\x12L\n\x10\x63onnection_state\x18\x07 \x01(\x0e\x32\x32.extensions.api.cast_channel.proto.ConnectionState\x12@\n\nread_state\x18\x08 \x01(\x0e\x32,.extensions.api.cast_channel.proto.ReadState\x12\x42\n\x0bwrite_state\x18\t \x01(\x0e\x32-.extensions.api.cast_channel.proto.WriteState\x12\x42\n\x0b\x65rror_state\x18\n \x01(\x0e\x32-.extensions.api.cast_channel.proto.ErrorState\x12^\n\x1a\x63hallenge_reply_error_type\x18\x0b \x01(\x0e\x32:.extensions.api.cast_channel.proto.ChallengeReplyErrorType\x12\x16\n\x0enss_error_code\x18\x0c \x01(\x05\"\xf4\x01\n\x15\x41ggregatedSocketEvent\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x13\n\x0b\x65ndpoint_id\x18\x02 \x01(\x05\x12I\n\x11\x63hannel_auth_type\x18\x03 \x01(\x0e\x32..extensions.api.cast_channel.proto.ChannelAuth\x12\x44\n\x0csocket_event\x18\x04 \x03(\x0b\x32..extensions.api.cast_channel.proto.SocketEvent\x12\x12\n\nbytes_read\x18\x05 \x01(\x03\x12\x15\n\rbytes_written\x18\x06 \x01(\x03\"\xb1\x01\n\x03Log\x12Y\n\x17\x61ggregated_socket_event\x18\x01 \x03(\x0b\x32\x38.extensions.api.cast_channel.proto.AggregatedSocketEvent\x12,\n$num_evicted_aggregated_socket_events\x18\x02 \x01(\x05\x12!\n\x19num_evicted_socket_events\x18\x03 \x01(\x05*\xc0\x06\n\tEventType\x12\x16\n\x12\x45VENT_TYPE_UNKNOWN\x10\x00\x12\x17\n\x13\x43\x41ST_SOCKET_CREATED\x10\x01\x12\x17\n\x13READY_STATE_CHANGED\x10\x02\x12\x1c\n\x18\x43ONNECTION_STATE_CHANGED\x10\x03\x12\x16\n\x12READ_STATE_CHANGED\x10\x04\x12\x17\n\x13WRITE_STATE_CHANGED\x10\x05\x12\x17\n\x13\x45RROR_STATE_CHANGED\x10\x06\x12\x12\n\x0e\x43ONNECT_FAILED\x10\x07\x12\x16\n\x12TCP_SOCKET_CONNECT\x10\x08\x12\x1d\n\x19TCP_SOCKET_SET_KEEP_ALIVE\x10\t\x12\x18\n\x14SSL_CERT_WHITELISTED\x10\n\x12\x16\n\x12SSL_SOCKET_CONNECT\x10\x0b\x12\x15\n\x11SSL_INFO_OBTAINED\x10\x0c\x12\x1b\n\x17\x44\x45R_ENCODED_CERT_OBTAIN\x10\r\x12\x1c\n\x18RECEIVED_CHALLENGE_REPLY\x10\x0e\x12\x18\n\x14\x41UTH_CHALLENGE_REPLY\x10\x0f\x12\x15\n\x11\x43ONNECT_TIMED_OUT\x10\x10\x12\x17\n\x13SEND_MESSAGE_FAILED\x10\x11\x12\x14\n\x10MESSAGE_ENQUEUED\x10\x12\x12\x10\n\x0cSOCKET_WRITE\x10\x13\x12\x13\n\x0fMESSAGE_WRITTEN\x10\x14\x12\x0f\n\x0bSOCKET_READ\x10\x15\x12\x10\n\x0cMESSAGE_READ\x10\x16\x12\x11\n\rSOCKET_CLOSED\x10\x19\x12\x1f\n\x1bSSL_CERT_EXCESSIVE_LIFETIME\x10\x1a\x12\x1b\n\x17\x43HANNEL_POLICY_ENFORCED\x10\x1b\x12\x1f\n\x1bTCP_SOCKET_CONNECT_COMPLETE\x10\x1c\x12\x1f\n\x1bSSL_SOCKET_CONNECT_COMPLETE\x10\x1d\x12\x1d\n\x19SSL_SOCKET_CONNECT_FAILED\x10\x1e\x12\x1e\n\x1aSEND_AUTH_CHALLENGE_FAILED\x10\x1f\x12 \n\x1c\x41UTH_CHALLENGE_REPLY_INVALID\x10 \x12\x14\n\x10PING_WRITE_ERROR\x10!*(\n\x0b\x43hannelAuth\x12\x07\n\x03SSL\x10\x01\x12\x10\n\x0cSSL_VERIFIED\x10\x02*\x85\x01\n\nReadyState\x12\x14\n\x10READY_STATE_NONE\x10\x01\x12\x1a\n\x16READY_STATE_CONNECTING\x10\x02\x12\x14\n\x10READY_STATE_OPEN\x10\x03\x12\x17\n\x13READY_STATE_CLOSING\x10\x04\x12\x16\n\x12READY_STATE_CLOSED\x10\x05*\x8f\x03\n\x0f\x43onnectionState\x12\x16\n\x12\x43ONN_STATE_UNKNOWN\x10\x01\x12\x1a\n\x16\x43ONN_STATE_TCP_CONNECT\x10\x02\x12#\n\x1f\x43ONN_STATE_TCP_CONNECT_COMPLETE\x10\x03\x12\x1a\n\x16\x43ONN_STATE_SSL_CONNECT\x10\x04\x12#\n\x1f\x43ONN_STATE_SSL_CONNECT_COMPLETE\x10\x05\x12\"\n\x1e\x43ONN_STATE_AUTH_CHALLENGE_SEND\x10\x06\x12+\n\'CONN_STATE_AUTH_CHALLENGE_SEND_COMPLETE\x10\x07\x12,\n(CONN_STATE_AUTH_CHALLENGE_REPLY_COMPLETE\x10\x08\x12\x1c\n\x18\x43ONN_STATE_START_CONNECT\x10\t\x12\x17\n\x13\x43ONN_STATE_FINISHED\x10\x64\x12\x14\n\x10\x43ONN_STATE_ERROR\x10\x65\x12\x16\n\x12\x43ONN_STATE_TIMEOUT\x10\x66*\xa5\x01\n\tReadState\x12\x16\n\x12READ_STATE_UNKNOWN\x10\x01\x12\x13\n\x0fREAD_STATE_READ\x10\x02\x12\x1c\n\x18READ_STATE_READ_COMPLETE\x10\x03\x12\x1a\n\x16READ_STATE_DO_CALLBACK\x10\x04\x12\x1b\n\x17READ_STATE_HANDLE_ERROR\x10\x05\x12\x14\n\x10READ_STATE_ERROR\x10\x64*\xc4\x01\n\nWriteState\x12\x17\n\x13WRITE_STATE_UNKNOWN\x10\x01\x12\x15\n\x11WRITE_STATE_WRITE\x10\x02\x12\x1e\n\x1aWRITE_STATE_WRITE_COMPLETE\x10\x03\x12\x1b\n\x17WRITE_STATE_DO_CALLBACK\x10\x04\x12\x1c\n\x18WRITE_STATE_HANDLE_ERROR\x10\x05\x12\x15\n\x11WRITE_STATE_ERROR\x10\x64\x12\x14\n\x10WRITE_STATE_IDLE\x10\x65*\xdb\x02\n\nErrorState\x12\x16\n\x12\x43HANNEL_ERROR_NONE\x10\x01\x12\"\n\x1e\x43HANNEL_ERROR_CHANNEL_NOT_OPEN\x10\x02\x12&\n\"CHANNEL_ERROR_AUTHENTICATION_ERROR\x10\x03\x12\x1f\n\x1b\x43HANNEL_ERROR_CONNECT_ERROR\x10\x04\x12\x1e\n\x1a\x43HANNEL_ERROR_SOCKET_ERROR\x10\x05\x12!\n\x1d\x43HANNEL_ERROR_TRANSPORT_ERROR\x10\x06\x12!\n\x1d\x43HANNEL_ERROR_INVALID_MESSAGE\x10\x07\x12$\n CHANNEL_ERROR_INVALID_CHANNEL_ID\x10\x08\x12!\n\x1d\x43HANNEL_ERROR_CONNECT_TIMEOUT\x10\t\x12\x19\n\x15\x43HANNEL_ERROR_UNKNOWN\x10\n*\xb0\x06\n\x17\x43hallengeReplyErrorType\x12\x1e\n\x1a\x43HALLENGE_REPLY_ERROR_NONE\x10\x01\x12)\n%CHALLENGE_REPLY_ERROR_PEER_CERT_EMPTY\x10\x02\x12,\n(CHALLENGE_REPLY_ERROR_WRONG_PAYLOAD_TYPE\x10\x03\x12$\n CHALLENGE_REPLY_ERROR_NO_PAYLOAD\x10\x04\x12\x30\n,CHALLENGE_REPLY_ERROR_PAYLOAD_PARSING_FAILED\x10\x05\x12\'\n#CHALLENGE_REPLY_ERROR_MESSAGE_ERROR\x10\x06\x12%\n!CHALLENGE_REPLY_ERROR_NO_RESPONSE\x10\x07\x12/\n+CHALLENGE_REPLY_ERROR_FINGERPRINT_NOT_FOUND\x10\x08\x12-\n)CHALLENGE_REPLY_ERROR_CERT_PARSING_FAILED\x10\t\x12\x37\n3CHALLENGE_REPLY_ERROR_CERT_NOT_SIGNED_BY_TRUSTED_CA\x10\n\x12\x33\n/CHALLENGE_REPLY_ERROR_CANNOT_EXTRACT_PUBLIC_KEY\x10\x0b\x12/\n+CHALLENGE_REPLY_ERROR_SIGNED_BLOBS_MISMATCH\x10\x0c\x12;\n7CHALLENGE_REPLY_ERROR_TLS_CERT_VALIDITY_PERIOD_TOO_LONG\x10\r\x12=\n9CHALLENGE_REPLY_ERROR_TLS_CERT_VALID_START_DATE_IN_FUTURE\x10\x0e\x12*\n&CHALLENGE_REPLY_ERROR_TLS_CERT_EXPIRED\x10\x0f\x12%\n!CHALLENGE_REPLY_ERROR_CRL_INVALID\x10\x10\x12&\n\"CHALLENGE_REPLY_ERROR_CERT_REVOKED\x10\x11\x42\x02H\x03') ) _sym_db.RegisterFileDescriptor(DESCRIPTOR) _EVENTTYPE = _descriptor.EnumDescriptor( name='EventType', full_name='extensions.api.cast_channel.proto.EventType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='EVENT_TYPE_UNKNOWN', index=0, number=0, options=None, type=None), _descriptor.EnumValueDescriptor( name='CAST_SOCKET_CREATED', index=1, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='READY_STATE_CHANGED', index=2, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONNECTION_STATE_CHANGED', index=3, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='READ_STATE_CHANGED', index=4, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='WRITE_STATE_CHANGED', index=5, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='ERROR_STATE_CHANGED', index=6, number=6, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONNECT_FAILED', index=7, number=7, options=None, type=None), _descriptor.EnumValueDescriptor( name='TCP_SOCKET_CONNECT', index=8, number=8, options=None, type=None), _descriptor.EnumValueDescriptor( name='TCP_SOCKET_SET_KEEP_ALIVE', index=9, number=9, options=None, type=None), _descriptor.EnumValueDescriptor( name='SSL_CERT_WHITELISTED', index=10, number=10, options=None, type=None), _descriptor.EnumValueDescriptor( name='SSL_SOCKET_CONNECT', index=11, number=11, options=None, type=None), _descriptor.EnumValueDescriptor( name='SSL_INFO_OBTAINED', index=12, number=12, options=None, type=None), _descriptor.EnumValueDescriptor( name='DER_ENCODED_CERT_OBTAIN', index=13, number=13, options=None, type=None), _descriptor.EnumValueDescriptor( name='RECEIVED_CHALLENGE_REPLY', index=14, number=14, options=None, type=None), _descriptor.EnumValueDescriptor( name='AUTH_CHALLENGE_REPLY', index=15, number=15, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONNECT_TIMED_OUT', index=16, number=16, options=None, type=None), _descriptor.EnumValueDescriptor( name='SEND_MESSAGE_FAILED', index=17, number=17, options=None, type=None), _descriptor.EnumValueDescriptor( name='MESSAGE_ENQUEUED', index=18, number=18, options=None, type=None), _descriptor.EnumValueDescriptor( name='SOCKET_WRITE', index=19, number=19, options=None, type=None), _descriptor.EnumValueDescriptor( name='MESSAGE_WRITTEN', index=20, number=20, options=None, type=None), _descriptor.EnumValueDescriptor( name='SOCKET_READ', index=21, number=21, options=None, type=None), _descriptor.EnumValueDescriptor( name='MESSAGE_READ', index=22, number=22, options=None, type=None), _descriptor.EnumValueDescriptor( name='SOCKET_CLOSED', index=23, number=25, options=None, type=None), _descriptor.EnumValueDescriptor( name='SSL_CERT_EXCESSIVE_LIFETIME', index=24, number=26, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_POLICY_ENFORCED', index=25, number=27, options=None, type=None), _descriptor.EnumValueDescriptor( name='TCP_SOCKET_CONNECT_COMPLETE', index=26, number=28, options=None, type=None), _descriptor.EnumValueDescriptor( name='SSL_SOCKET_CONNECT_COMPLETE', index=27, number=29, options=None, type=None), _descriptor.EnumValueDescriptor( name='SSL_SOCKET_CONNECT_FAILED', index=28, number=30, options=None, type=None), _descriptor.EnumValueDescriptor( name='SEND_AUTH_CHALLENGE_FAILED', index=29, number=31, options=None, type=None), _descriptor.EnumValueDescriptor( name='AUTH_CHALLENGE_REPLY_INVALID', index=30, number=32, options=None, type=None), _descriptor.EnumValueDescriptor( name='PING_WRITE_ERROR', index=31, number=33, options=None, type=None), ], containing_type=None, options=None, serialized_start=1120, serialized_end=1952, ) _sym_db.RegisterEnumDescriptor(_EVENTTYPE) EventType = enum_type_wrapper.EnumTypeWrapper(_EVENTTYPE) _CHANNELAUTH = _descriptor.EnumDescriptor( name='ChannelAuth', full_name='extensions.api.cast_channel.proto.ChannelAuth', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='SSL', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='SSL_VERIFIED', index=1, number=2, options=None, type=None), ], containing_type=None, options=None, serialized_start=1954, serialized_end=1994, ) _sym_db.RegisterEnumDescriptor(_CHANNELAUTH) ChannelAuth = enum_type_wrapper.EnumTypeWrapper(_CHANNELAUTH) _READYSTATE = _descriptor.EnumDescriptor( name='ReadyState', full_name='extensions.api.cast_channel.proto.ReadyState', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='READY_STATE_NONE', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='READY_STATE_CONNECTING', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='READY_STATE_OPEN', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='READY_STATE_CLOSING', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='READY_STATE_CLOSED', index=4, number=5, options=None, type=None), ], containing_type=None, options=None, serialized_start=1997, serialized_end=2130, ) _sym_db.RegisterEnumDescriptor(_READYSTATE) ReadyState = enum_type_wrapper.EnumTypeWrapper(_READYSTATE) _CONNECTIONSTATE = _descriptor.EnumDescriptor( name='ConnectionState', full_name='extensions.api.cast_channel.proto.ConnectionState', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='CONN_STATE_UNKNOWN', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_TCP_CONNECT', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_TCP_CONNECT_COMPLETE', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_SSL_CONNECT', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_SSL_CONNECT_COMPLETE', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_AUTH_CHALLENGE_SEND', index=5, number=6, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_AUTH_CHALLENGE_SEND_COMPLETE', index=6, number=7, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_AUTH_CHALLENGE_REPLY_COMPLETE', index=7, number=8, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_START_CONNECT', index=8, number=9, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_FINISHED', index=9, number=100, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_ERROR', index=10, number=101, options=None, type=None), _descriptor.EnumValueDescriptor( name='CONN_STATE_TIMEOUT', index=11, number=102, options=None, type=None), ], containing_type=None, options=None, serialized_start=2133, serialized_end=2532, ) _sym_db.RegisterEnumDescriptor(_CONNECTIONSTATE) ConnectionState = enum_type_wrapper.EnumTypeWrapper(_CONNECTIONSTATE) _READSTATE = _descriptor.EnumDescriptor( name='ReadState', full_name='extensions.api.cast_channel.proto.ReadState', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='READ_STATE_UNKNOWN', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='READ_STATE_READ', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='READ_STATE_READ_COMPLETE', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='READ_STATE_DO_CALLBACK', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='READ_STATE_HANDLE_ERROR', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='READ_STATE_ERROR', index=5, number=100, options=None, type=None), ], containing_type=None, options=None, serialized_start=2535, serialized_end=2700, ) _sym_db.RegisterEnumDescriptor(_READSTATE) ReadState = enum_type_wrapper.EnumTypeWrapper(_READSTATE) _WRITESTATE = _descriptor.EnumDescriptor( name='WriteState', full_name='extensions.api.cast_channel.proto.WriteState', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='WRITE_STATE_UNKNOWN', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='WRITE_STATE_WRITE', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='WRITE_STATE_WRITE_COMPLETE', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='WRITE_STATE_DO_CALLBACK', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='WRITE_STATE_HANDLE_ERROR', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='WRITE_STATE_ERROR', index=5, number=100, options=None, type=None), _descriptor.EnumValueDescriptor( name='WRITE_STATE_IDLE', index=6, number=101, options=None, type=None), ], containing_type=None, options=None, serialized_start=2703, serialized_end=2899, ) _sym_db.RegisterEnumDescriptor(_WRITESTATE) WriteState = enum_type_wrapper.EnumTypeWrapper(_WRITESTATE) _ERRORSTATE = _descriptor.EnumDescriptor( name='ErrorState', full_name='extensions.api.cast_channel.proto.ErrorState', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_NONE', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_CHANNEL_NOT_OPEN', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_AUTHENTICATION_ERROR', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_CONNECT_ERROR', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_SOCKET_ERROR', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_TRANSPORT_ERROR', index=5, number=6, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_INVALID_MESSAGE', index=6, number=7, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_INVALID_CHANNEL_ID', index=7, number=8, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_CONNECT_TIMEOUT', index=8, number=9, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHANNEL_ERROR_UNKNOWN', index=9, number=10, options=None, type=None), ], containing_type=None, options=None, serialized_start=2902, serialized_end=3249, ) _sym_db.RegisterEnumDescriptor(_ERRORSTATE) ErrorState = enum_type_wrapper.EnumTypeWrapper(_ERRORSTATE) _CHALLENGEREPLYERRORTYPE = _descriptor.EnumDescriptor( name='ChallengeReplyErrorType', full_name='extensions.api.cast_channel.proto.ChallengeReplyErrorType', filename=None, file=DESCRIPTOR, values=[ _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_NONE', index=0, number=1, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_PEER_CERT_EMPTY', index=1, number=2, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_WRONG_PAYLOAD_TYPE', index=2, number=3, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_NO_PAYLOAD', index=3, number=4, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_PAYLOAD_PARSING_FAILED', index=4, number=5, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_MESSAGE_ERROR', index=5, number=6, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_NO_RESPONSE', index=6, number=7, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_FINGERPRINT_NOT_FOUND', index=7, number=8, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_CERT_PARSING_FAILED', index=8, number=9, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_CERT_NOT_SIGNED_BY_TRUSTED_CA', index=9, number=10, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_CANNOT_EXTRACT_PUBLIC_KEY', index=10, number=11, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_SIGNED_BLOBS_MISMATCH', index=11, number=12, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_TLS_CERT_VALIDITY_PERIOD_TOO_LONG', index=12, number=13, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_TLS_CERT_VALID_START_DATE_IN_FUTURE', index=13, number=14, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_TLS_CERT_EXPIRED', index=14, number=15, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_CRL_INVALID', index=15, number=16, options=None, type=None), _descriptor.EnumValueDescriptor( name='CHALLENGE_REPLY_ERROR_CERT_REVOKED', index=16, number=17, options=None, type=None), ], containing_type=None, options=None, serialized_start=3252, serialized_end=4068, ) _sym_db.RegisterEnumDescriptor(_CHALLENGEREPLYERRORTYPE) ChallengeReplyErrorType = enum_type_wrapper.EnumTypeWrapper(_CHALLENGEREPLYERRORTYPE) EVENT_TYPE_UNKNOWN = 0 CAST_SOCKET_CREATED = 1 READY_STATE_CHANGED = 2 CONNECTION_STATE_CHANGED = 3 READ_STATE_CHANGED = 4 WRITE_STATE_CHANGED = 5 ERROR_STATE_CHANGED = 6 CONNECT_FAILED = 7 TCP_SOCKET_CONNECT = 8 TCP_SOCKET_SET_KEEP_ALIVE = 9 SSL_CERT_WHITELISTED = 10 SSL_SOCKET_CONNECT = 11 SSL_INFO_OBTAINED = 12 DER_ENCODED_CERT_OBTAIN = 13 RECEIVED_CHALLENGE_REPLY = 14 AUTH_CHALLENGE_REPLY = 15 CONNECT_TIMED_OUT = 16 SEND_MESSAGE_FAILED = 17 MESSAGE_ENQUEUED = 18 SOCKET_WRITE = 19 MESSAGE_WRITTEN = 20 SOCKET_READ = 21 MESSAGE_READ = 22 SOCKET_CLOSED = 25 SSL_CERT_EXCESSIVE_LIFETIME = 26 CHANNEL_POLICY_ENFORCED = 27 TCP_SOCKET_CONNECT_COMPLETE = 28 SSL_SOCKET_CONNECT_COMPLETE = 29 SSL_SOCKET_CONNECT_FAILED = 30 SEND_AUTH_CHALLENGE_FAILED = 31 AUTH_CHALLENGE_REPLY_INVALID = 32 PING_WRITE_ERROR = 33 SSL = 1 SSL_VERIFIED = 2 READY_STATE_NONE = 1 READY_STATE_CONNECTING = 2 READY_STATE_OPEN = 3 READY_STATE_CLOSING = 4 READY_STATE_CLOSED = 5 CONN_STATE_UNKNOWN = 1 CONN_STATE_TCP_CONNECT = 2 CONN_STATE_TCP_CONNECT_COMPLETE = 3 CONN_STATE_SSL_CONNECT = 4 CONN_STATE_SSL_CONNECT_COMPLETE = 5 CONN_STATE_AUTH_CHALLENGE_SEND = 6 CONN_STATE_AUTH_CHALLENGE_SEND_COMPLETE = 7 CONN_STATE_AUTH_CHALLENGE_REPLY_COMPLETE = 8 CONN_STATE_START_CONNECT = 9 CONN_STATE_FINISHED = 100 CONN_STATE_ERROR = 101 CONN_STATE_TIMEOUT = 102 READ_STATE_UNKNOWN = 1 READ_STATE_READ = 2 READ_STATE_READ_COMPLETE = 3 READ_STATE_DO_CALLBACK = 4 READ_STATE_HANDLE_ERROR = 5 READ_STATE_ERROR = 100 WRITE_STATE_UNKNOWN = 1 WRITE_STATE_WRITE = 2 WRITE_STATE_WRITE_COMPLETE = 3 WRITE_STATE_DO_CALLBACK = 4 WRITE_STATE_HANDLE_ERROR = 5 WRITE_STATE_ERROR = 100 WRITE_STATE_IDLE = 101 CHANNEL_ERROR_NONE = 1 CHANNEL_ERROR_CHANNEL_NOT_OPEN = 2 CHANNEL_ERROR_AUTHENTICATION_ERROR = 3 CHANNEL_ERROR_CONNECT_ERROR = 4 CHANNEL_ERROR_SOCKET_ERROR = 5 CHANNEL_ERROR_TRANSPORT_ERROR = 6 CHANNEL_ERROR_INVALID_MESSAGE = 7 CHANNEL_ERROR_INVALID_CHANNEL_ID = 8 CHANNEL_ERROR_CONNECT_TIMEOUT = 9 CHANNEL_ERROR_UNKNOWN = 10 CHALLENGE_REPLY_ERROR_NONE = 1 CHALLENGE_REPLY_ERROR_PEER_CERT_EMPTY = 2 CHALLENGE_REPLY_ERROR_WRONG_PAYLOAD_TYPE = 3 CHALLENGE_REPLY_ERROR_NO_PAYLOAD = 4 CHALLENGE_REPLY_ERROR_PAYLOAD_PARSING_FAILED = 5 CHALLENGE_REPLY_ERROR_MESSAGE_ERROR = 6 CHALLENGE_REPLY_ERROR_NO_RESPONSE = 7 CHALLENGE_REPLY_ERROR_FINGERPRINT_NOT_FOUND = 8 CHALLENGE_REPLY_ERROR_CERT_PARSING_FAILED = 9 CHALLENGE_REPLY_ERROR_CERT_NOT_SIGNED_BY_TRUSTED_CA = 10 CHALLENGE_REPLY_ERROR_CANNOT_EXTRACT_PUBLIC_KEY = 11 CHALLENGE_REPLY_ERROR_SIGNED_BLOBS_MISMATCH = 12 CHALLENGE_REPLY_ERROR_TLS_CERT_VALIDITY_PERIOD_TOO_LONG = 13 CHALLENGE_REPLY_ERROR_TLS_CERT_VALID_START_DATE_IN_FUTURE = 14 CHALLENGE_REPLY_ERROR_TLS_CERT_EXPIRED = 15 CHALLENGE_REPLY_ERROR_CRL_INVALID = 16 CHALLENGE_REPLY_ERROR_CERT_REVOKED = 17 _SOCKETEVENT = _descriptor.Descriptor( name='SocketEvent', full_name='extensions.api.cast_channel.proto.SocketEvent', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='type', full_name='extensions.api.cast_channel.proto.SocketEvent.type', index=0, number=1, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='timestamp_micros', full_name='extensions.api.cast_channel.proto.SocketEvent.timestamp_micros', index=1, number=2, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='details', full_name='extensions.api.cast_channel.proto.SocketEvent.details', index=2, number=3, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='net_return_value', full_name='extensions.api.cast_channel.proto.SocketEvent.net_return_value', index=3, number=4, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='message_namespace', full_name='extensions.api.cast_channel.proto.SocketEvent.message_namespace', index=4, number=5, type=9, cpp_type=9, label=1, has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='ready_state', full_name='extensions.api.cast_channel.proto.SocketEvent.ready_state', index=5, number=6, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='connection_state', full_name='extensions.api.cast_channel.proto.SocketEvent.connection_state', index=6, number=7, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='read_state', full_name='extensions.api.cast_channel.proto.SocketEvent.read_state', index=7, number=8, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='write_state', full_name='extensions.api.cast_channel.proto.SocketEvent.write_state', index=8, number=9, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='error_state', full_name='extensions.api.cast_channel.proto.SocketEvent.error_state', index=9, number=10, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='challenge_reply_error_type', full_name='extensions.api.cast_channel.proto.SocketEvent.challenge_reply_error_type', index=10, number=11, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='nss_error_code', full_name='extensions.api.cast_channel.proto.SocketEvent.nss_error_code', index=11, number=12, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=53, serialized_end=690, ) _AGGREGATEDSOCKETEVENT = _descriptor.Descriptor( name='AggregatedSocketEvent', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='id', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.id', index=0, number=1, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='endpoint_id', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.endpoint_id', index=1, number=2, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='channel_auth_type', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.channel_auth_type', index=2, number=3, type=14, cpp_type=8, label=1, has_default_value=False, default_value=1, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='socket_event', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.socket_event', index=3, number=4, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='bytes_read', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.bytes_read', index=4, number=5, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='bytes_written', full_name='extensions.api.cast_channel.proto.AggregatedSocketEvent.bytes_written', index=5, number=6, type=3, cpp_type=2, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=693, serialized_end=937, ) _LOG = _descriptor.Descriptor( name='Log', full_name='extensions.api.cast_channel.proto.Log', filename=None, file=DESCRIPTOR, containing_type=None, fields=[ _descriptor.FieldDescriptor( name='aggregated_socket_event', full_name='extensions.api.cast_channel.proto.Log.aggregated_socket_event', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='num_evicted_aggregated_socket_events', full_name='extensions.api.cast_channel.proto.Log.num_evicted_aggregated_socket_events', index=1, number=2, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), _descriptor.FieldDescriptor( name='num_evicted_socket_events', full_name='extensions.api.cast_channel.proto.Log.num_evicted_socket_events', index=2, number=3, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), ], extensions=[ ], nested_types=[], enum_types=[ ], options=None, is_extendable=False, syntax='proto2', extension_ranges=[], oneofs=[ ], serialized_start=940, serialized_end=1117, ) _SOCKETEVENT.fields_by_name['type'].enum_type = _EVENTTYPE _SOCKETEVENT.fields_by_name['ready_state'].enum_type = _READYSTATE _SOCKETEVENT.fields_by_name['connection_state'].enum_type = _CONNECTIONSTATE _SOCKETEVENT.fields_by_name['read_state'].enum_type = _READSTATE _SOCKETEVENT.fields_by_name['write_state'].enum_type = _WRITESTATE _SOCKETEVENT.fields_by_name['error_state'].enum_type = _ERRORSTATE _SOCKETEVENT.fields_by_name['challenge_reply_error_type'].enum_type = _CHALLENGEREPLYERRORTYPE _AGGREGATEDSOCKETEVENT.fields_by_name['channel_auth_type'].enum_type = _CHANNELAUTH _AGGREGATEDSOCKETEVENT.fields_by_name['socket_event'].message_type = _SOCKETEVENT _LOG.fields_by_name['aggregated_socket_event'].message_type = _AGGREGATEDSOCKETEVENT DESCRIPTOR.message_types_by_name['SocketEvent'] = _SOCKETEVENT DESCRIPTOR.message_types_by_name['AggregatedSocketEvent'] = _AGGREGATEDSOCKETEVENT DESCRIPTOR.message_types_by_name['Log'] = _LOG DESCRIPTOR.enum_types_by_name['EventType'] = _EVENTTYPE DESCRIPTOR.enum_types_by_name['ChannelAuth'] = _CHANNELAUTH DESCRIPTOR.enum_types_by_name['ReadyState'] = _READYSTATE DESCRIPTOR.enum_types_by_name['ConnectionState'] = _CONNECTIONSTATE DESCRIPTOR.enum_types_by_name['ReadState'] = _READSTATE DESCRIPTOR.enum_types_by_name['WriteState'] = _WRITESTATE DESCRIPTOR.enum_types_by_name['ErrorState'] = _ERRORSTATE DESCRIPTOR.enum_types_by_name['ChallengeReplyErrorType'] = _CHALLENGEREPLYERRORTYPE SocketEvent = _reflection.GeneratedProtocolMessageType('SocketEvent', (_message.Message,), dict( DESCRIPTOR = _SOCKETEVENT, __module__ = 'logging_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.proto.SocketEvent) )) _sym_db.RegisterMessage(SocketEvent) AggregatedSocketEvent = _reflection.GeneratedProtocolMessageType('AggregatedSocketEvent', (_message.Message,), dict( DESCRIPTOR = _AGGREGATEDSOCKETEVENT, __module__ = 'logging_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.proto.AggregatedSocketEvent) )) _sym_db.RegisterMessage(AggregatedSocketEvent) Log = _reflection.GeneratedProtocolMessageType('Log', (_message.Message,), dict( DESCRIPTOR = _LOG, __module__ = 'logging_pb2' # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.proto.Log) )) _sym_db.RegisterMessage(Log) DESCRIPTOR.has_options = True DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('H\003')) # @@protoc_insertion_point(module_scope) pychromecast-4.1.0/pychromecast/socket_client.py000066400000000000000000001250161355177132200221310ustar00rootroot00000000000000""" Module to interact with the ChromeCast via protobuf-over-socket. Big thanks goes out to Fred Clift who build the first version of this code: https://github.com/minektur/chromecast-python-poc. Without him this would not have been possible. """ # Pylint does not understand the protobuf objects correctly # pylint: disable=no-member, too-many-lines import errno import json import logging import select import socket import ssl import sys import threading import time from collections import namedtuple from struct import pack, unpack from . import cast_channel_pb2 from .controllers import BaseController from .controllers.media import MediaController from .dial import CAST_TYPE_CHROMECAST, CAST_TYPE_AUDIO, CAST_TYPE_GROUP from .discovery import get_info_from_service, get_host_from_service_info from .error import ( ChromecastConnectionError, UnsupportedNamespace, NotConnected, PyChromecastStopped, LaunchError, ) NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection" NS_RECEIVER = "urn:x-cast:com.google.cast.receiver" NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat" PLATFORM_DESTINATION_ID = "receiver-0" MESSAGE_TYPE = "type" TYPE_PING = "PING" TYPE_RECEIVER_STATUS = "RECEIVER_STATUS" TYPE_PONG = "PONG" TYPE_CONNECT = "CONNECT" TYPE_CLOSE = "CLOSE" TYPE_GET_STATUS = "GET_STATUS" TYPE_LAUNCH = "LAUNCH" TYPE_LAUNCH_ERROR = "LAUNCH_ERROR" TYPE_LOAD = "LOAD" # The socket connection is being setup CONNECTION_STATUS_CONNECTING = "CONNECTING" # The socket connection was complete CONNECTION_STATUS_CONNECTED = "CONNECTED" # The socket connection has been disconnected CONNECTION_STATUS_DISCONNECTED = "DISCONNECTED" # Connecting to socket failed (after a CONNECTION_STATUS_CONNECTING) CONNECTION_STATUS_FAILED = "FAILED" # Failed to resolve service name CONNECTION_STATUS_FAILED_RESOLVE = "FAILED_RESOLVE" # The socket connection was lost and needs to be retried CONNECTION_STATUS_LOST = "LOST" APP_ID = "appId" REQUEST_ID = "requestId" SESSION_ID = "sessionId" ERROR_REASON = "reason" HB_PING_TIME = 10 HB_PONG_TIME = 10 POLL_TIME_BLOCKING = 5.0 POLL_TIME_NON_BLOCKING = 0.01 TIMEOUT_TIME = 30 RETRY_TIME = 5 class InterruptLoop(Exception): """ The chromecast has been manually stopped. """ def _json_from_message(message): """ Parses a PB2 message into JSON format. """ try: return json.loads(message.payload_utf8) except ValueError: logger = logging.getLogger(__name__) logger.warning( "Ignoring invalid json in namespace %s: %s", message.namespace, message.payload_utf8, ) return {} def _message_to_string(message, data=None): """ Gives a string representation of a PB2 message. """ if data is None: data = _json_from_message(message) return "Message {} from {} to {}: {}".format( message.namespace, message.source_id, message.destination_id, data ) if sys.version_info >= (3, 0): def _json_to_payload(data): """ Encodes a python value into JSON format. """ return json.dumps(data, ensure_ascii=False).encode("utf8") else: def _json_to_payload(data): """ Encodes a python value into JSON format. """ return json.dumps(data, ensure_ascii=False) def _is_ssl_timeout(exc): """ Returns True if the exception is for an SSL timeout """ return exc.message in ( "The handshake operation timed out", "The write operation timed out", "The read operation timed out", ) NetworkAddress = namedtuple("NetworkAddress", ["address", "port"]) ConnectionStatus = namedtuple("ConnectionStatus", ["status", "address"]) CastStatus = namedtuple( "CastStatus", [ "is_active_input", "is_stand_by", "volume_level", "volume_muted", "app_id", "display_name", "namespaces", "session_id", "transport_id", "status_text", "icon_url", ], ) LaunchFailure = namedtuple("LaunchStatus", ["reason", "app_id", "request_id"]) # pylint: disable=too-many-instance-attributes class SocketClient(threading.Thread): """ Class to interact with a Chromecast through a socket. :param port: The port to use when connecting to the device, set to None to use the default of 8009. Special devices such as Cast Groups may return a different port number so we need to use that. :param cast_type: The type of chromecast to connect to, see dial.CAST_TYPE_* for types. :param tries: Number of retries to perform if the connection fails. None for inifinite retries. :param retry_wait: A floating point number specifying how many seconds to wait between each retry. None means to use the default which is 5 seconds. """ def __init__(self, host, port=None, cast_type=CAST_TYPE_CHROMECAST, **kwargs): tries = kwargs.pop("tries", None) timeout = kwargs.pop("timeout", None) retry_wait = kwargs.pop("retry_wait", None) self.blocking = kwargs.pop("blocking", True) services = kwargs.pop("services", None) zconf = kwargs.pop("zconf", None) if self.blocking: self.polltime = POLL_TIME_BLOCKING else: self.polltime = POLL_TIME_NON_BLOCKING super(SocketClient, self).__init__() self.daemon = True self.logger = logging.getLogger(__name__) self._force_recon = False self.cast_type = cast_type self.fn = None # pylint:disable=invalid-name self.tries = tries self.timeout = timeout or TIMEOUT_TIME self.retry_wait = retry_wait or RETRY_TIME self.host = host self.services = services or [None] self.zconf = zconf self.port = port or 8009 self.source_id = "sender-0" self.stop = threading.Event() self.app_namespaces = [] self.destination_id = None self.session_id = None self._request_id = 0 # dict mapping requestId on threading.Event objects self._request_callbacks = {} self._open_channels = [] self.connecting = True self.socket = None # dict mapping namespace on Controller objects self._handlers = {} self._connection_listeners = [] self.receiver_controller = ReceiverController(cast_type, self.blocking) self.media_controller = MediaController() self.heartbeat_controller = HeartbeatController() self.register_handler(self.heartbeat_controller) self.register_handler(ConnectionController()) self.register_handler(self.receiver_controller) self.register_handler(self.media_controller) self.receiver_controller.register_status_listener(self) def initialize_connection( self ): # noqa: E501 pylint:disable=too-many-statements, too-many-branches """Initialize a socket to a Chromecast, retrying as necessary.""" tries = self.tries if self.socket is not None: self.socket.close() self.socket = None # Make sure nobody is blocking. for callback in self._request_callbacks.values(): callback["event"].set() self.app_namespaces = [] self.destination_id = None self.session_id = None self._request_id = 0 self._request_callbacks = {} self._open_channels = [] self.connecting = True retry_log_fun = self.logger.error # Dict keeping track of individual retry delay for each named service retries = {} def mdns_backoff(service, retry): """Exponentional backoff for service name mdns lookups.""" now = time.time() retry["next_retry"] = now + retry["delay"] retry["delay"] = min(retry["delay"] * 2, 300) retries[service] = retry while not self.stop.is_set() and ( tries is None or tries > 0 ): # noqa: E501 pylint:disable=too-many-nested-blocks # Prune retries dict retries = { key: retries[key] for key in self.services if (key is not None and key in retries) } for service in self.services.copy(): now = time.time() retry = retries.get( service, {"delay": self.retry_wait, "next_retry": now} ) # If we're connecting to a named service, check if it's time if service and now < retry["next_retry"]: continue try: self.socket = new_socket() self.socket.settimeout(self.timeout) self._report_connection_status( ConnectionStatus( CONNECTION_STATUS_CONNECTING, NetworkAddress(self.host, self.port), ) ) # Resolve the service name. If service is None, we're # connecting directly to a host name or IP-address if service: host = None port = None service_info = get_info_from_service(service, self.zconf) host, port = get_host_from_service_info(service_info) if host and port: try: self.fn = service_info.properties[b"fn"].decode("utf-8") except (AttributeError, KeyError, UnicodeError): pass self.logger.debug( "[%s:%s] Resolved service %s to %s:%s", self.fn or self.host, self.port, service, host, port, ) self.host = host self.port = port else: self.logger.debug( "[%s:%s] Failed to resolve service %s", self.fn or self.host, self.port, service, ) self._report_connection_status( ConnectionStatus( CONNECTION_STATUS_FAILED_RESOLVE, NetworkAddress(service, None), ) ) mdns_backoff(service, retry) # If zeroconf fails to receive the necessary data, # try next service continue self.logger.debug( "[%s:%s] Connecting to %s:%s", self.fn or self.host, self.port, self.host, self.port, ) self.socket.connect((self.host, self.port)) self.socket = ssl.wrap_socket(self.socket) self.connecting = False self._force_recon = False self._report_connection_status( ConnectionStatus( CONNECTION_STATUS_CONNECTED, NetworkAddress(self.host, self.port), ) ) self.receiver_controller.update_status() self.heartbeat_controller.ping() self.heartbeat_controller.reset() self.logger.debug( "[%s:%s] Connected!", self.fn or self.host, self.port ) return except OSError as err: self.connecting = True if self.stop.is_set(): self.logger.error( "[%s:%s] Failed to connect: %s. " "aborting due to stop signal.", self.fn or self.host, self.port, err, ) raise ChromecastConnectionError("Failed to connect") self._report_connection_status( ConnectionStatus( CONNECTION_STATUS_FAILED, NetworkAddress(self.host, self.port), ) ) if service is not None: retry_log_fun( "[%s:%s] Failed to connect to service %s" ", retrying in %.1fs", self.fn or self.host, self.port, service, retry["delay"], ) mdns_backoff(service, retry) else: retry_log_fun( "[%s:%s] Failed to connect, retrying in %.1fs", self.fn or self.host, self.port, self.retry_wait, ) retry_log_fun = self.logger.debug # Only sleep if we have another retry remaining if tries is None or tries > 1: self.logger.debug( "[%s:%s] Not connected, sleeping for %.1fs. Services: %s", self.fn or self.host, self.port, self.retry_wait, self.services, ) time.sleep(self.retry_wait) if tries: tries -= 1 self.stop.set() self.logger.error( "[%s:%s] Failed to connect. No retries.", self.fn or self.host, self.port ) raise ChromecastConnectionError("Failed to connect") def connect(self): """ Connect socket connection to Chromecast device. Must only be called if the worker thread will not be started. """ try: self.initialize_connection() except ChromecastConnectionError: self._report_connection_status( ConnectionStatus( CONNECTION_STATUS_DISCONNECTED, NetworkAddress(self.host, self.port) ) ) return def disconnect(self): """ Disconnect socket connection to Chromecast device """ self.stop.set() def register_handler(self, handler): """ Register a new namespace handler. """ self._handlers[handler.namespace] = handler handler.registered(self) def new_cast_status(self, cast_status): """ Called when a new cast status has been received. """ new_channel = self.destination_id != cast_status.transport_id if new_channel: self.disconnect_channel(self.destination_id) self.app_namespaces = cast_status.namespaces self.destination_id = cast_status.transport_id self.session_id = cast_status.session_id if new_channel: # If any of the namespaces of the new app are supported # we will automatically connect to it to receive updates for namespace in self.app_namespaces: if namespace in self._handlers: self._ensure_channel_connected(self.destination_id) self._handlers[namespace].channel_connected() def _gen_request_id(self): """ Generates a unique request id. """ self._request_id += 1 return self._request_id @property def is_connected(self): """ Returns True if the client is connected, False if it is stopped (or trying to connect). """ return not self.connecting @property def is_stopped(self): """ Returns True if the connection has been stopped, False if it is running. """ return self.stop.is_set() def run(self): """ Connect to the cast and start polling the socket. """ try: self.initialize_connection() except ChromecastConnectionError: self._report_connection_status( ConnectionStatus( CONNECTION_STATUS_DISCONNECTED, NetworkAddress(self.host, self.port) ) ) return self.heartbeat_controller.reset() self._force_recon = False logging.debug("Thread started...") while not self.stop.is_set(): if self.run_once() == 1: break # Clean up self._cleanup() def run_once(self): """ Use run_once() in your own main loop after you receive something on the socket (get_socket()). """ # pylint: disable=too-many-branches, too-many-return-statements try: if not self._check_connection(): return 0 except ChromecastConnectionError: return 1 # poll the socket can_read, _, _ = select.select([self.socket], [], [], self.polltime) # read messages from chromecast message = data = None if self.socket in can_read and not self._force_recon: try: message = self._read_message() except InterruptLoop as exc: if self.stop.is_set(): self.logger.info( "[%s:%s] Stopped while reading message, " "disconnecting.", self.fn or self.host, self.port, ) else: self.logger.error( "[%s:%s] Interruption caught without being stopped: " "%s", self.fn or self.host, self.port, exc, ) return 1 except ssl.SSLError as exc: if exc.errno == ssl.SSL_ERROR_EOF: if self.stop.is_set(): return 1 raise except socket.error: self._force_recon = True self.logger.error( "[%s:%s] Error reading from socket.", self.fn or self.host, self.port, ) else: data = _json_from_message(message) if not message: return 0 # If we are stopped after receiving a message we skip the message # and tear down the connection if self.stop.is_set(): return 1 # See if any handlers will accept this message self._route_message(message, data) if REQUEST_ID in data: callback = self._request_callbacks.pop(data[REQUEST_ID], None) if callback is not None: event = callback["event"] callback["response"] = data function = callback["function"] event.set() if function: function(data) return 0 def get_socket(self): """ Returns the socket of the connection to use it in you own main loop. """ return self.socket def _check_connection(self): """ Checks if the connection is active, and if not reconnect :return: True if the connection is active, False if the connection was reset. """ # check if connection is expired reset = False if self._force_recon: self.logger.warning( "[%s:%s] Error communicating with socket, resetting " "connection", self.fn or self.host, self.port, ) reset = True elif self.heartbeat_controller.is_expired(): self.logger.warning( "[%s:%s] Heartbeat timeout, resetting connection", self.fn or self.host, self.port, ) reset = True if reset: for channel in self._open_channels: self.disconnect_channel(channel) self._report_connection_status( ConnectionStatus( CONNECTION_STATUS_LOST, NetworkAddress(self.host, self.port) ) ) try: self.initialize_connection() except ChromecastConnectionError: self.stop.set() return False return True def _route_message(self, message, data): """ Route message to any handlers on the message namespace """ # route message to handlers if message.namespace in self._handlers: # debug messages if message.namespace != NS_HEARTBEAT: self.logger.debug( "[%s:%s] Received: %s", self.fn or self.host, self.port, _message_to_string(message, data), ) # message handlers try: handled = self._handlers[message.namespace].receive_message( message, data ) if not handled: if data.get(REQUEST_ID) not in self._request_callbacks: self.logger.debug( "[%s:%s] Message unhandled: %s", self.fn or self.host, self.port, _message_to_string(message, data), ) except Exception: # pylint: disable=broad-except self.logger.exception( ( "[%s:%s] Exception caught while sending message to " "controller %s: %s" ), self.fn or self.host, self.port, type(self._handlers[message.namespace]).__name__, _message_to_string(message, data), ) else: self.logger.debug( "[%s:%s] Received unknown namespace: %s", self.fn or self.host, self.port, _message_to_string(message, data), ) def _cleanup(self): """ Cleanup open channels and handlers """ for channel in self._open_channels: try: self.disconnect_channel(channel) except Exception: # pylint: disable=broad-except pass for handler in self._handlers.values(): try: handler.tear_down() except Exception: # pylint: disable=broad-except pass try: self.socket.close() except Exception: # pylint: disable=broad-except self.logger.exception("[%s:%s] _cleanup", self.fn or self.host, self.port) self._report_connection_status( ConnectionStatus( CONNECTION_STATUS_DISCONNECTED, NetworkAddress(self.host, self.port) ) ) self.connecting = True def _report_connection_status(self, status): """ Report a change in the connection status to any listeners """ for listener in self._connection_listeners: try: self.logger.debug( "[%s:%s] connection listener: %x (%s)", self.fn or self.host, self.port, id(listener), type(listener).__name__, ) listener.new_connection_status(status) except Exception: # pylint: disable=broad-except self.logger.exception( "[%s:%s] Exception thrown when calling connection " "listener", self.fn or self.host, self.port, ) def _read_bytes_from_socket(self, msglen): """ Read bytes from the socket. """ chunks = [] bytes_recd = 0 while bytes_recd < msglen: if self.stop.is_set(): raise InterruptLoop("Stopped while reading from socket") try: chunk = self.socket.recv(min(msglen - bytes_recd, 2048)) if chunk == b"": raise socket.error("socket connection broken") chunks.append(chunk) bytes_recd += len(chunk) except socket.timeout: continue except ssl.SSLError as exc: # Support older ssl implementations which does not raise # socket.timeout on timeouts if _is_ssl_timeout(exc): continue raise return b"".join(chunks) def _read_message(self): """ Reads a message from the socket and converts it to a message. """ # first 4 bytes is Big-Endian payload length payload_info = self._read_bytes_from_socket(4) read_len = unpack(">I", payload_info)[0] # now read the payload payload = self._read_bytes_from_socket(read_len) # pylint: disable=no-member message = cast_channel_pb2.CastMessage() message.ParseFromString(payload) return message # pylint: disable=too-many-arguments def send_message( self, destination_id, namespace, data, inc_session_id=False, callback_function=False, no_add_request_id=False, force=False, ): """ Send a message to the Chromecast. """ # namespace is a string containing namespace # data is a dict that will be converted to json # wait_for_response only works if we have a request id # If channel is not open yet, connect to it. self._ensure_channel_connected(destination_id) request_id = None if not no_add_request_id: request_id = self._gen_request_id() data[REQUEST_ID] = request_id if inc_session_id: data[SESSION_ID] = self.session_id # pylint: disable=no-member msg = cast_channel_pb2.CastMessage() msg.protocol_version = msg.CASTV2_1_0 msg.source_id = self.source_id msg.destination_id = destination_id msg.payload_type = cast_channel_pb2.CastMessage.STRING msg.namespace = namespace msg.payload_utf8 = _json_to_payload(data) # prepend message with Big-Endian 4 byte payload size be_size = pack(">I", msg.ByteSize()) # Log all messages except heartbeat if msg.namespace != NS_HEARTBEAT: self.logger.debug( "[%s:%s] Sending: %s", self.fn or self.host, self.port, _message_to_string(msg, data), ) if not force and self.stop.is_set(): raise PyChromecastStopped("Socket client's thread is stopped.") if not self.connecting and not self._force_recon: try: if not no_add_request_id and callback_function: self._request_callbacks[request_id] = { "event": threading.Event(), "response": None, "function": callback_function, } self.socket.sendall(be_size + msg.SerializeToString()) except socket.error: self._request_callbacks.pop(request_id, None) self._force_recon = True self.logger.info( "[%s:%s] Error writing to socket.", self.fn or self.host, self.port ) else: raise NotConnected( "Chromecast {}:{} is connecting...".format(self.host, self.port) ) def send_platform_message( self, namespace, message, inc_session_id=False, callback_function_param=False ): """ Helper method to send a message to the platform. """ return self.send_message( PLATFORM_DESTINATION_ID, namespace, message, inc_session_id, callback_function_param, ) def send_app_message( self, namespace, message, inc_session_id=False, callback_function_param=False ): """ Helper method to send a message to current running app. """ if namespace not in self.app_namespaces: raise UnsupportedNamespace( ( "Namespace {} is not supported by current app. " "Supported are {}" ).format(namespace, ", ".join(self.app_namespaces)) ) return self.send_message( self.destination_id, namespace, message, inc_session_id, callback_function_param, ) def register_connection_listener(self, listener): """ Register a connection listener for when the socket connection changes. Listeners will be called with listener.new_connection_status(status) """ self._connection_listeners.append(listener) def _ensure_channel_connected(self, destination_id): """ Ensure we opened a channel to destination_id. """ if destination_id not in self._open_channels: self._open_channels.append(destination_id) self.send_message( destination_id, NS_CONNECTION, { MESSAGE_TYPE: TYPE_CONNECT, "origin": {}, "userAgent": "PyChromecast", "senderInfo": { "sdkType": 2, "version": "15.605.1.3", "browserVersion": "44.0.2403.30", "platform": 4, "systemVersion": "Macintosh; Intel Mac OS X10_10_3", "connectionType": 1, }, }, no_add_request_id=True, ) def disconnect_channel(self, destination_id): """ Disconnect a channel with destination_id. """ if destination_id in self._open_channels: try: self.send_message( destination_id, NS_CONNECTION, {MESSAGE_TYPE: TYPE_CLOSE, "origin": {}}, no_add_request_id=True, force=True, ) except NotConnected: pass except Exception: # pylint: disable=broad-except self.logger.exception( "[%s:%s] Exception", self.fn or self.host, self.port ) self._open_channels.remove(destination_id) self.handle_channel_disconnected() def handle_channel_disconnected(self): """ Handles a channel being disconnected. """ for namespace in self.app_namespaces: if namespace in self._handlers: self._handlers[namespace].channel_disconnected() self.app_namespaces = [] self.destination_id = None self.session_id = None class ConnectionController(BaseController): """ Controller to respond to connection messages. """ def __init__(self): super(ConnectionController, self).__init__(NS_CONNECTION) def receive_message(self, message, data): """ Called when a connection message is received. """ if self._socket_client.is_stopped: return True if data[MESSAGE_TYPE] == TYPE_CLOSE: # The cast device is asking us to acknowledge closing this channel. self._socket_client.disconnect_channel(message.source_id) # Schedule a status update so that a channel is created. self._socket_client.receiver_controller.update_status() return True return False class HeartbeatController(BaseController): """ Controller to respond to heartbeat messages. """ def __init__(self): super(HeartbeatController, self).__init__(NS_HEARTBEAT, target_platform=True) self.last_ping = 0 self.last_pong = time.time() def receive_message(self, message, data): """ Called when a heartbeat message is received. """ if self._socket_client.is_stopped: return True if data[MESSAGE_TYPE] == TYPE_PING: try: self._socket_client.send_message( PLATFORM_DESTINATION_ID, self.namespace, {MESSAGE_TYPE: TYPE_PONG}, no_add_request_id=True, ) except PyChromecastStopped: self._socket_client.logger.debug( "Heartbeat error when sending response, " "Chromecast connection has stopped" ) return True if data[MESSAGE_TYPE] == TYPE_PONG: self.reset() return True return False def ping(self): """ Send a ping message. """ self.last_ping = time.time() try: self.send_message({MESSAGE_TYPE: TYPE_PING}) except NotConnected: self._socket_client.logger.error( "Chromecast is disconnected. " "Cannot ping until reconnected." ) def reset(self): """ Reset expired counter. """ self.last_pong = time.time() def is_expired(self): """ Indicates if connection has expired. """ if time.time() - self.last_ping > HB_PING_TIME: self.ping() return (time.time() - self.last_pong) > HB_PING_TIME + HB_PONG_TIME class ReceiverController(BaseController): """ Controller to interact with the Chromecast platform. :param cast_type: Type of Chromecast device. """ def __init__(self, cast_type=CAST_TYPE_CHROMECAST, blocking=True): super(ReceiverController, self).__init__(NS_RECEIVER, target_platform=True) self.status = None self.launch_failure = None self.app_to_launch = None self.cast_type = cast_type self.blocking = blocking self.app_launch_event = threading.Event() self.app_launch_event_function = None self._status_listeners = [] self._launch_error_listeners = [] @property def app_id(self): """ Convenience method to retrieve current app id. """ return self.status.app_id if self.status else None def receive_message(self, message, data): """ Called when a receiver-message has been received. """ if data[MESSAGE_TYPE] == TYPE_RECEIVER_STATUS: self._process_get_status(data) return True if data[MESSAGE_TYPE] == TYPE_LAUNCH_ERROR: self._process_launch_error(data) return True return False def register_status_listener(self, listener): """ Register a status listener for when a new Chromecast status has been received. Listeners will be called with listener.new_cast_status(status) """ self._status_listeners.append(listener) def register_launch_error_listener(self, listener): """ Register a listener for when a new launch error message has been received. Listeners will be called with listener.new_launch_error(launch_failure) """ self._launch_error_listeners.append(listener) def update_status(self, callback_function_param=False): """ Sends a message to the Chromecast to update the status. """ self.logger.debug("Receiver:Updating status") self.send_message( {MESSAGE_TYPE: TYPE_GET_STATUS}, callback_function=callback_function_param ) def launch_app(self, app_id, force_launch=False, callback_function=False): """ Launches an app on the Chromecast. Will only launch if it is not currently running unless force_launch=True. """ if not force_launch and self.app_id is None: self.update_status( lambda response: self._send_launch_message( app_id, force_launch, callback_function ) ) else: self._send_launch_message(app_id, force_launch, callback_function) def _send_launch_message(self, app_id, force_launch=False, callback_function=False): if force_launch or self.app_id != app_id: self.logger.info("Receiver:Launching app %s", app_id) self.app_to_launch = app_id self.app_launch_event.clear() self.app_launch_event_function = callback_function self.launch_failure = None self.send_message( {MESSAGE_TYPE: TYPE_LAUNCH, APP_ID: app_id}, callback_function=lambda response: self._block_till_launched(app_id), ) else: self.logger.info("Not launching app %s - already running", app_id) if callback_function: callback_function() def _block_till_launched(self, app_id): if self.blocking: self.app_launch_event.wait() if self.launch_failure: raise LaunchError( "Failed to launch app: {}, Reason: {}".format( app_id, self.launch_failure.reason ) ) def stop_app(self, callback_function_param=False): """ Stops the current running app on the Chromecast. """ self.logger.info("Receiver:Stopping current app '%s'", self.app_id) return self.send_message( {MESSAGE_TYPE: "STOP"}, inc_session_id=True, callback_function=callback_function_param, ) def set_volume(self, volume): """ Allows to set volume. Should be value between 0..1. Returns the new volume. """ volume = min(max(0, volume), 1) self.logger.info("Receiver:setting volume to %.1f", volume) self.send_message({MESSAGE_TYPE: "SET_VOLUME", "volume": {"level": volume}}) return volume def set_volume_muted(self, muted): """ Allows to mute volume. """ self.send_message({MESSAGE_TYPE: "SET_VOLUME", "volume": {"muted": muted}}) @staticmethod def _parse_status(data, cast_type): """ Parses a STATUS message and returns a CastStatus object. :type data: dict :param cast_type: Type of Chromecast. :rtype: CastStatus """ data = data.get("status", {}) volume_data = data.get("volume", {}) try: app_data = data["applications"][0] except (KeyError, IndexError): app_data = {} is_audio = cast_type in (CAST_TYPE_AUDIO, CAST_TYPE_GROUP) status = CastStatus( data.get("isActiveInput", None if is_audio else False), data.get("isStandBy", None if is_audio else True), volume_data.get("level", 1.0), volume_data.get("muted", False), app_data.get(APP_ID), app_data.get("displayName"), [item["name"] for item in app_data.get("namespaces", [])], app_data.get(SESSION_ID), app_data.get("transportId"), app_data.get("statusText", ""), app_data.get("iconUrl"), ) return status def _process_get_status(self, data): """ Processes a received STATUS message and notifies listeners. """ status = self._parse_status(data, self.cast_type) is_new_app = self.app_id != status.app_id and self.app_to_launch self.status = status self.logger.debug("Received status: %s", self.status) self._report_status() if is_new_app and self.app_to_launch == self.app_id: self.app_to_launch = None self.app_launch_event.set() if self.app_launch_event_function: self.logger.debug("Start app_launch_event_function...") self.app_launch_event_function() self.app_launch_event_function = None def _report_status(self): """ Reports the current status to all listeners. """ for listener in self._status_listeners: try: listener.new_cast_status(self.status) except Exception: # pylint: disable=broad-except self.logger.exception( "Exception thrown when calling cast status listener" ) @staticmethod def _parse_launch_error(data): """ Parses a LAUNCH_ERROR message and returns a LaunchFailure object. :type data: dict :rtype: LaunchFailure """ return LaunchFailure( data.get(ERROR_REASON, None), data.get(APP_ID), data.get(REQUEST_ID) ) def _process_launch_error(self, data): """ Processes a received LAUNCH_ERROR message and notifies listeners. """ launch_failure = self._parse_launch_error(data) self.launch_failure = launch_failure if self.app_to_launch: self.app_to_launch = None self.app_launch_event.set() self.logger.debug("Launch status: %s", launch_failure) for listener in self._launch_error_listeners: try: listener.new_launch_error(launch_failure) except Exception: # pylint: disable=broad-except self.logger.exception( "Exception thrown when calling launch error listener" ) def tear_down(self): """ Called when controller is destroyed. """ super(ReceiverController, self).tear_down() self.status = None self.launch_failure = None self.app_to_launch = None self.app_launch_event.clear() self._status_listeners[:] = [] def new_socket(): """ Create a new socket with OS-specific parameters Try to set SO_REUSEPORT for BSD-flavored systems if it's an option. Catches errors if not. """ new_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) new_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: # noinspection PyUnresolvedReferences reuseport = socket.SO_REUSEPORT except AttributeError: pass else: try: new_sock.setsockopt(socket.SOL_SOCKET, reuseport, 1) except (OSError, socket.error) as err: # OSError on python 3, socket.error on python 2 if err.errno != errno.ENOPROTOOPT: raise return new_sock pychromecast-4.1.0/pylintrc000066400000000000000000000004721355177132200160150ustar00rootroot00000000000000[MASTER] ignore=cast_channel_pb2.py,authority_keys_pb2.py,logging_pb2.py reports=no disable= format, locally-disabled, too-many-arguments, too-many-instance-attributes, too-many-public-methods, duplicate-code, too-many-nested-blocks, [EXCEPTIONS] overgeneral-exceptions=Exception,PyChromecastError pychromecast-4.1.0/pyproject.toml000066400000000000000000000000351355177132200171350ustar00rootroot00000000000000[tool.black] exclude = 'pb2' pychromecast-4.1.0/requirements.txt000066400000000000000000000000771355177132200175130ustar00rootroot00000000000000requests>=2.0 protobuf>=3.0.0 zeroconf>=0.17.7 casttube>=0.2.0 pychromecast-4.1.0/script/000077500000000000000000000000001355177132200155275ustar00rootroot00000000000000pychromecast-4.1.0/script/release000077500000000000000000000002111355177132200170670ustar00rootroot00000000000000#!/bin/sh # Pushes a new version to PyPi. rm -rf dist python3 setup.py sdist bdist_wheel python3 -m twine upload dist/* --skip-existing pychromecast-4.1.0/setup.cfg000066400000000000000000000004321355177132200160430ustar00rootroot00000000000000[wheel] universal = 1 [flake8] # To work with Black max-line-length = 88 # E501: line too long # W503: Line break occurred before a binary operator # E203: Whitespace before ':' # D202 No blank lines allowed after function docstring ignore = E501, W503, E203, D202 pychromecast-4.1.0/setup.py000066400000000000000000000016621355177132200157420ustar00rootroot00000000000000from setuptools import setup, find_packages long_description = open("README.rst").read() setup( name="PyChromecast", version="4.1.0", license="MIT", url="https://github.com/balloob/pychromecast", author="Paulus Schoutsen", author_email="paulus@paulusschoutsen.nl", description="Python module to talk to Google Chromecast.", long_description=long_description, packages=find_packages(), zip_safe=False, include_package_data=True, platforms="any", install_requires=list(val.strip() for val in open("requirements.txt")), classifiers=[ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries :: Python Modules", ], )