Skip to content

Commit

Permalink
Merge pull request #8 from berryk - Publish topics using friendly_nam…
Browse files Browse the repository at this point in the history
…e and not IP
  • Loading branch information
nohum authored Aug 15, 2017
2 parents 21ad605 + 326fc6a commit c697b68
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 90 deletions.
54 changes: 27 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,52 +13,52 @@ You can install the requirements in their correct versions using `pip3 install -

## 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.


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`, `SKIP` 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!
46 changes: 23 additions & 23 deletions handler/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand All @@ -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


Expand All @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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):
# <MediaStatus {'media_metadata': {}, 'content_id': 'http://some.url.com/', 'player_state': 'PLAYING',
Expand All @@ -380,7 +380,7 @@ def _worker_cast_media_status(self, status):
# 'supports_skip_backward': False, 'stream_type': 'BUFFERED', 'playback_rate': 1,
# 'supports_skip_forward': False, 'season': None, 'duration': None, 'images': [], 'series_title': None,
# 'supports_seek': True, 'current_time': 13938.854693, 'supported_media_commands': 15}>
self.logger.info("received new media status from chromecast %s" % self.ip_address)
self.logger.info("received new media status from chromecast %s" % self.device_name)

images = status.media_metadata.get('images', [])
image_filtered = None
Expand Down
72 changes: 36 additions & 36 deletions handler/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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)
17 changes: 13 additions & 4 deletions helper/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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)

0 comments on commit c697b68

Please sign in to comment.