From 7caef98c0c51bf454f7e390cacb0badb13989ee9 Mon Sep 17 00:00:00 2001 From: berryk Date: Sat, 12 Aug 2017 12:52:28 -0400 Subject: [PATCH 1/4] Publishes using friendly_name rather than IP Publishes topics using friendly_name instead of ip. This has the added benefit of allowing Chromecast Groups to work. --- README.md | 48 +++++++++++++++--------------- handler/adapter.py | 46 ++++++++++++++--------------- handler/event.py | 72 ++++++++++++++++++++++----------------------- helper/discovery.py | 17 ++++++++--- 4 files changed, 96 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index e52df46..3c6bfc2 100644 --- a/README.md +++ b/README.md @@ -9,38 +9,38 @@ Provides status information and control capabilities of your Chromecast devices * paho-mqtt * Zeroconf -You can install the requirements in their correct versions using `pip3 install -r requirements.txt`. +You can install the requirements in their correct versions using `pfriendly_name3 install -r requirements.txt`. ## Discovery and control -Using MQTT you can find the following topics. `IP` is the ip address used to connect +Using MQTT you can find the following topics. `friendly_name` is the name used to connect to each Chromecast. ``` # - read only -chromecast/IP/friendly_name -chromecast/IP/connection_status -chromecast/IP/cast_type -chromecast/IP/current_app -chromecast/IP/player_duration -chromecast/IP/player_position -chromecast/IP/player_state -chromecast/IP/volume_level -chromecast/IP/volume_muted -chromecast/IP/media/title -chromecast/IP/media/album_name -chromecast/IP/media/artist -chromecast/IP/media/album_artist -chromecast/IP/media/track -chromecast/IP/media/images -chromecast/IP/media/content_type -chromecast/IP/media/content_url +chromecast/friendly_name/friendly_name +chromecast/friendly_name/connection_status +chromecast/friendly_name/cast_type +chromecast/friendly_name/current_app +chromecast/friendly_name/player_duration +chromecast/friendly_name/player_position +chromecast/friendly_name/player_state +chromecast/friendly_name/volume_level +chromecast/friendly_name/volume_muted +chromecast/friendly_name/media/title +chromecast/friendly_name/media/album_name +chromecast/friendly_name/media/artist +chromecast/friendly_name/media/album_artist +chromecast/friendly_name/media/track +chromecast/friendly_name/media/images +chromecast/friendly_name/media/content_type +chromecast/friendly_name/media/content_url # - writable -chromecast/IP/command/volume_level -chromecast/IP/command/volume_muted -chromecast/IP/command/player_position -chromecast/IP/command/player_state +chromecast/friendly_name/command/volume_level +chromecast/friendly_name/command/volume_muted +chromecast/friendly_name/command/player_position +chromecast/friendly_name/command/player_state ``` Control the player by publishing values to the four topics above. @@ -60,5 +60,5 @@ Play something: Publish a json array with two elements (content url and content You can also just publish a URL to `player_state` (just as string, not as json array, e.g. `http://your.stream.url.here`), the application then tries to guess the required MIME type. -For other player controls, simply publish e.g. `RESUME`, `PAUSE`, `STOP`, `SKIP` or `REWIND` to +For other player controls, simply publish e.g. `RESUME`, `PAUSE`, `STOP`, `SKfriendly_name` or `REWIND` to `chromecast/192.168.0.1/command/player_state`. Attention: This is case-sensitive! diff --git a/handler/adapter.py b/handler/adapter.py index 0bac3fa..fd4b9c9 100644 --- a/handler/adapter.py +++ b/handler/adapter.py @@ -12,7 +12,7 @@ CONNECTION_STATUS_ERROR = "ERROR" CONNECTION_STATUS_NOT_FOUND = "NOT_FOUND" -CreateConnectionCommand = namedtuple("CreateConnectionCommand", ["ip_address"]) +CreateConnectionCommand = namedtuple("CreateConnectionCommand", ["device_name"]) DisconnectCommand = namedtuple("DisconnectCommand", []) VolumeMuteCommand = namedtuple("VolumeMuteCommand", ["muted"]) VolumeLevelRelativeCommand = namedtuple("VolumeLevelRelativeCommand", ["value"]) @@ -32,10 +32,10 @@ class ChromecastConnectionCallback: - def on_connection_failed(self, chromecast_connection, ip_address): + def on_connection_failed(self, chromecast_connection, device_name): pass - def on_connection_dead(self, chromecast_connection, ip_address): + def on_connection_dead(self, chromecast_connection, device_name): pass @@ -48,25 +48,25 @@ class ConnectionUnavailableException(Exception): class ChromecastConnection(MqttChangesCallback): - def __init__(self, ip_address, mqtt_connection, connection_callback): + def __init__(self, device_name, mqtt_connection, connection_callback): """ Called if a new Chromecast device has been found. """ self.logger = logging.getLogger("chromecast") - self.ip_address = ip_address + self.device_name = device_name self.connection_callback = connection_callback self.connection_failure_count = 0 self.device_connected = False - self.mqtt_properties = MqttPropertyHandler(mqtt_connection, ip_address, self) + self.mqtt_properties = MqttPropertyHandler(mqtt_connection, device_name, self) self.processing_queue = Queue(maxsize=100) self.processing_worker = Thread(target=self._worker) self.processing_worker.daemon = True self.processing_worker.start() - self.processing_queue.put(CreateConnectionCommand(ip_address)) + self.processing_queue.put(CreateConnectionCommand(device_name)) def is_connected(self): # TODO thread sync @@ -105,7 +105,7 @@ def new_launch_error(self, launch_failure): PyChromecast error callback. """ - self.logger.error("received error from chromecast %s: %s" % (self.ip_address, launch_failure)) + self.logger.error("received error from chromecast %s: %s" % (self.device_name, launch_failure)) def new_connection_status(self, status): """ @@ -166,14 +166,14 @@ def _worker(self): if requires_connection and not self.device_connected: self.logger.info("no connection found but connection is required") - self._internal_create_connection(self.ip_address) + self._internal_create_connection(self.device_name) if not self.device_connected: self.logger.error("was not able to connect to device for command %s" % (item,)) raise ConnectionUnavailableException() if isinstance(item, CreateConnectionCommand): - self._worker_create_connection(item.ip_address) + self._worker_create_connection(item.device_name) elif isinstance(item, DisconnectCommand): self._worker_disconnect() elif isinstance(item, VolumeMuteCommand): @@ -213,25 +213,25 @@ def _worker(self): # e.g. AttributeError: 'NoneType' object has no attribute 'media_controller' # at least something indicating that the connection is really dead for sure if isinstance(error, AttributeError): - self.connection_callback.on_connection_dead(self, self.ip_address) + self.connection_callback.on_connection_dead(self, self.device_name) else: - self.connection_callback.on_connection_failed(self, self.ip_address) + self.connection_callback.on_connection_failed(self, self.device_name) finally: self.logger.debug("command %s finished" % (item,)) self.processing_queue.task_done() - def _internal_create_connection(self, ip_address): + def _internal_create_connection(self, device_name): try: self.mqtt_properties.write_connection_status(CONNECTION_STATUS_WAITING_FOR_DEVICE) devices = get_chromecasts(tries=5) # TODO not the best way to do this, change with #3 for device in devices: - if device.host == ip_address: + if device.device.friendly_name == device_name: self.device = device break if self.device is None: - self.logger.error("was not able to find chromecast %s" % self.ip_address) + self.logger.error("was not able to find chromecast %s" % self.device_name) raise ConnectionUnavailableException() self.device.register_status_listener(self) @@ -241,19 +241,19 @@ def _internal_create_connection(self, ip_address): self.device_connected = True # alibi action except ChromecastConnectionError: - self.logger.exception("had connection error while finding chromecast %s" % self.ip_address) + self.logger.exception("had connection error while finding chromecast %s" % self.device_name) self.device_connected = False - def _worker_create_connection(self, ip_address): + def _worker_create_connection(self, device_name): # uncaught exceptions bubble to the try-except handler of the worker thread - self._internal_create_connection(ip_address) + self._internal_create_connection(device_name) if not self.device_connected: self.mqtt_properties.write_connection_status(CONNECTION_STATUS_ERROR) def _worker_disconnect(self): - self.logger.info("disconnecting chromecast %s" % self.ip_address) + self.logger.info("disconnecting chromecast %s" % self.device_name) self.device_connected = False @@ -337,7 +337,7 @@ def _worker_cast_received_status(self, status): # CastStatus(is_active_input=None, is_stand_by=None, volume_level=0.3499999940395355, volume_muted=False, # app_id='CC1AD845', display_name='Default Media Receiver', namespaces=['urn:x-cast:com.google.cast.media'], # session_id='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx', transport_id='web-0', status_text='Now Casting') - self.logger.info("received new cast status from chromecast %s" % self.ip_address) + self.logger.info("received new cast status from chromecast %s" % self.device_name) if status is None: self.logger.warning("received empty status") @@ -353,7 +353,7 @@ def _worker_cast_received_status(self, status): self.mqtt_properties.write_player_status(MEDIA_PLAYER_STATE_IDLE, None, None) def _worker_cast_connection_status(self, status): - self.logger.info("received new connection status from chromecast %s: %s" % (self.ip_address, status.status)) + self.logger.info("received new connection status from chromecast %s: %s" % (self.device_name, status.status)) self.mqtt_properties.write_connection_status(status.status) self.device_connected = status.status == CONNECTION_STATUS_CONNECTED @@ -369,7 +369,7 @@ def _worker_cast_connection_status(self, status): if self.connection_failure_count > 7: self.logger.warning("failure counter too high, treating chromecast as dead") - self.connection_callback.on_connection_dead(self, self.ip_address) + self.connection_callback.on_connection_dead(self, self.device_name) def _worker_cast_media_status(self, status): # - self.logger.info("received new media status from chromecast %s" % self.ip_address) + self.logger.info("received new media status from chromecast %s" % self.device_name) images = status.media_metadata.get('images', []) image_filtered = None diff --git a/handler/event.py b/handler/event.py index a6ec335..af4eff3 100644 --- a/handler/event.py +++ b/handler/event.py @@ -9,10 +9,10 @@ from threading import Thread MqttMessage = namedtuple("MqttMessage", ["topic", "payload"]) -DeviceAppeared = namedtuple("DeviceAppeared", ["ip_address"]) -DeviceDisappeared = namedtuple("DeviceDisappeared", ["ip_address"]) -DeviceConnectionFailure = namedtuple("DeviceConnectionFailure", ["ip_address", "connection"]) -DeviceConnectionDead = namedtuple("DeviceConnectionDead", ["ip_address", "connection"]) +DeviceAppeared = namedtuple("DeviceAppeared", ["device_name"]) +DeviceDisappeared = namedtuple("DeviceDisappeared", ["device_name"]) +DeviceConnectionFailure = namedtuple("DeviceConnectionFailure", ["device_name", "connection"]) +DeviceConnectionDead = namedtuple("DeviceConnectionDead", ["device_name", "connection"]) class SortedPriorityQueue(PriorityQueue): @@ -66,16 +66,16 @@ def on_mqtt_message_received(self, topic, payload): self.processing_queue.put(MqttMessage(topic, payload), 2) def on_chromecast_appeared(self, device_name, model_name, ip_address, port): - self.processing_queue.put(DeviceAppeared(ip_address), 0) + self.processing_queue.put(DeviceAppeared(device_name), 0) - def on_chromecast_disappeared(self, ip_address): - self.processing_queue.put(DeviceDisappeared(ip_address), 0) + def on_chromecast_disappeared(self, device_name): + self.processing_queue.put(DeviceDisappeared(device_name), 0) - def on_connection_failed(self, chromecast_connection, ip_address): - self.processing_queue.put(DeviceConnectionFailure(ip_address, chromecast_connection), 2) + def on_connection_failed(self, chromecast_connection, device_name): + self.processing_queue.put(DeviceConnectionFailure(device_name, chromecast_connection), 2) - def on_connection_dead(self, chromecast_connection, ip_address): - self.processing_queue.put(DeviceConnectionDead(ip_address, chromecast_connection), 0) + def on_connection_dead(self, chromecast_connection, device_name): + self.processing_queue.put(DeviceConnectionDead(device_name, chromecast_connection), 0) def _worker(self): while True: @@ -85,13 +85,13 @@ def _worker(self): if isinstance(item, MqttMessage): self._worker_mqtt_message_received(item.topic, item.payload) elif isinstance(item, DeviceAppeared): - self._worker_chromecast_appeared(item.ip_address) + self._worker_chromecast_appeared(item.device_name) elif isinstance(item, DeviceDisappeared): - self._worker_chromecast_disappeared(item.ip_address) + self._worker_chromecast_disappeared(item.device_name) elif isinstance(item, DeviceConnectionFailure): - self._worker_chromecast_connection_failed(item.ip_address, item.connection) + self._worker_chromecast_connection_failed(item.device_name, item.connection) elif isinstance(item, DeviceConnectionDead): - self._worker_chromecast_connection_dead(item.ip_address, item.connection) + self._worker_chromecast_connection_dead(item.device_name, item.connection) except: self.logger.exception("event %s failed" % (item,)) finally: @@ -111,41 +111,41 @@ def _worker_mqtt_message_received(self, topic, payload): # topic is e.g. "chromecast/%s/command/volume_level" parts = topic.split("/") if len(parts) > 2: - ip_address = parts[1] - device = ChromecastConnection(ip_address, self.mqtt_client, self) + device_name = parts[1] + device = ChromecastConnection(device_name, self.mqtt_client, self) - self.known_devices[ip_address] = device - self.logger.info("added device %s after receiving topic addressing it" % ip_address) + self.known_devices[device_name] = device + self.logger.info("added device %s after receiving topic addressing it" % device_name) device.handle_message(topic, payload) - def _worker_chromecast_appeared(self, ip_address): - if ip_address in self.known_devices: - self.logger.warning("device %s already known" % ip_address) + def _worker_chromecast_appeared(self, device_name): + if device_name in self.known_devices: + self.logger.warning("device %s already known" % device_name) return - self.known_devices[ip_address] = ChromecastConnection(ip_address, self.mqtt_client, self) - self.logger.info("added device %s" % ip_address) + self.known_devices[device_name] = ChromecastConnection(device_name, self.mqtt_client, self) + self.logger.info("added device %s" % device_name) - def _worker_chromecast_disappeared(self, ip_address): - if ip_address not in self.known_devices: - self.logger.warning("device %s not known" % ip_address) + def _worker_chromecast_disappeared(self, device_name): + if device_name not in self.known_devices: + self.logger.warning("device %s not known" % device_name) return - device = self.known_devices[ip_address] + device = self.known_devices[device_name] if device.is_connected(): - self.logger.warning("device %s is still connected and not removed" % ip_address) + self.logger.warning("device %s is still connected and not removed" % device_name) else: - self.logger.debug("de-registering device %s" % ip_address) + self.logger.debug("de-registering device %s" % device_name) - self.known_devices.pop(ip_address) # ignore result, we already have the device + self.known_devices.pop(device_name) # ignore result, we already have the device device.unregister_device() - def _worker_chromecast_connection_failed(self, ip_address, connection): - self.logger.warning("connection to device %s failed too often" % ip_address) + def _worker_chromecast_connection_failed(self, device_name, connection): + self.logger.warning("connection to device %s failed too often" % device_name) # TODO if the connection fails to often, treat it as dead - def _worker_chromecast_connection_dead(self, ip_address, connection): - self.logger.error("connection to device %s is dead, removing" % ip_address) - self.known_devices.pop(ip_address) + def _worker_chromecast_connection_dead(self, device_name, connection): + self.logger.error("connection to device %s is dead, removing" % device_name) + self.known_devices.pop(device_name) diff --git a/helper/discovery.py b/helper/discovery.py index 2492441..e4b3f3a 100644 --- a/helper/discovery.py +++ b/helper/discovery.py @@ -12,7 +12,7 @@ class DiscoveryCallback: def on_chromecast_appeared(self, device_name, model_name, ip_address, port): pass - def on_chromecast_disappeared(self, ip_address): + def on_chromecast_disappeared(self, device_name): pass @@ -92,8 +92,17 @@ def add_service(self, zconf, typ, name): ips = zconf.cache.entries_with_name(service.server.lower()) host = repr(ips[0]) if ips else service.server - model_name = service.properties.get('md') - device_name = service.properties.get('fn') + def get_value(key): + """Retrieve value and decode for Python 2/3.""" + value = service.properties.get(key.encode('utf-8')) - self.services[name] = host + #if value is None or isinstance(value, six.text_type): + # return value + return value.decode('utf-8') + + model_name = get_value('md') + device_name = get_value('fn') + self.logger.info("chromecast device name \"%s\"" % device_name) + + self.services[name] = device_name self.discovery_callback.on_chromecast_appeared(device_name, model_name, host, service.port) From efe477a164fb13b9ba72ecc8c796c7c01aeb9410 Mon Sep 17 00:00:00 2001 From: berryk Date: Sat, 12 Aug 2017 12:53:40 -0400 Subject: [PATCH 2/4] More changes to README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3c6bfc2..d26a356 100644 --- a/README.md +++ b/README.md @@ -48,17 +48,17 @@ Control the player by publishing values to the four topics above. Change volume using values from `0` to `100`: -* Absolute: publish e.g. `55` to `chromecast/192.168.0.1/command/volume_level` -* Relative: publish e.g. `+5` or `-5` to `chromecast/192.168.0.1/command/volume_level` +* Absolute: publish e.g. `55` to `chromecast/friendly_name/command/volume_level` +* Relative: publish e.g. `+5` or `-5` to `chromecast/friendly_name/command/volume_level` -Change mute state: publish `0` or `1` to `chromecast/192.168.0.1/command/volume_muted`. +Change mute state: publish `0` or `1` to `chromecast/friendly_name/command/volume_muted`. Play something: Publish a json array with two elements (content url and content type) to -`chromecast/192.168.0.1/command/player_state`, e.g. `["http://your.stream.url.here", "audio/mpeg"]`. +`chromecast/friendly_name/command/player_state`, e.g. `["http://your.stream.url.here", "audio/mpeg"]`. You can also just publish a URL to `player_state` (just as string, not as json array, e.g. `http://your.stream.url.here`), the application then tries to guess the required MIME type. For other player controls, simply publish e.g. `RESUME`, `PAUSE`, `STOP`, `SKfriendly_name` or `REWIND` to -`chromecast/192.168.0.1/command/player_state`. Attention: This is case-sensitive! +`chromecast/friendly_name/command/player_state`. Attention: This is case-sensitive! From bd76a8b031f8ce16534cddf8233ca3feac1a941c Mon Sep 17 00:00:00 2001 From: berryk Date: Sat, 12 Aug 2017 12:54:20 -0400 Subject: [PATCH 3/4] And another typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d26a356..b1ffca1 100644 --- a/README.md +++ b/README.md @@ -60,5 +60,5 @@ Play something: Publish a json array with two elements (content url and content You can also just publish a URL to `player_state` (just as string, not as json array, e.g. `http://your.stream.url.here`), the application then tries to guess the required MIME type. -For other player controls, simply publish e.g. `RESUME`, `PAUSE`, `STOP`, `SKfriendly_name` or `REWIND` to +For other player controls, simply publish e.g. `RESUME`, `PAUSE`, `STOP`, `SKIP` or `REWIND` to `chromecast/friendly_name/command/player_state`. Attention: This is case-sensitive! From 326fc6af2ec4d1859ecbbc90b8e10a22c0e61be9 Mon Sep 17 00:00:00 2001 From: berryk Date: Sun, 13 Aug 2017 22:14:06 -0400 Subject: [PATCH 4/4] Fixed typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b1ffca1..15f29a8 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Provides status information and control capabilities of your Chromecast devices * paho-mqtt * Zeroconf -You can install the requirements in their correct versions using `pfriendly_name3 install -r requirements.txt`. +You can install the requirements in their correct versions using `pip3 install -r requirements.txt`. ## Discovery and control