from threading import Thread import os, logging, subprocess, socket, json, random, string, select # import logger logger = logging.getLogger(__name__) class IrisSnapcast(object): socket = None listen = False def __init__(self, config): self.config = config self.path = os.path.dirname(__file__) ## # This thread has been started ## def connect(self): host = str(self.config['iris']['snapcast_host']) port = int(self.config['iris']['snapcast_port']) try: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.settimeout(10) self.sock.connect((host, port)) logger.debug("Snapcast connection established on "+host+":"+str(port)) except socket.gaierror, e: raise Exception(e); except socket.error, e: raise Exception(e); ## # Listen for socket activity # Broadcast events to the piped broadcaster # # @param broadcast = Method ## def listen(self, broadcast): self.connect() self.listen = True logger.info("Established Snapcast listener") broadcast(data={'method':'snapcast_connected'}) messages = [] data = "" select.select([], [self.sock], []) while self.listen: # This loop will run constantly in the background, so we need a relatively short # timeout as this would otherwise block the shutdown sequence timeout = 1 readlist, writelist, exceptionlist = select.select([self.sock], [], [], timeout) # Check if we've got any lists if [readlist, writelist, exceptionlist] != [[], [], []]: # Rread and print the available data on any of the read list for socket in readlist: # Allow a relatively large buffer size to handle large JSON payloads message = socket.recv(8192) try: message = json.loads(message) # Prefix with snapcast message['method'] = "snapcast_"+message['method'] # Broadcast to all clients broadcast(data=message) except: logger.error("Malformed Snapcast message: "+message) ## # Stop our listener # # @param broadcast = Method ## def stop_listening(self): self.listen = False ## # Send a single request to Snapcast # # @param data = Dict ## def request(self, data): # No socket to make request on if self.sock == None: raise Exception("Socket not established"); # Construct our request, based on the provided data request = { 'id': self.generateGuid(), 'jsonrpc': '2.0', 'method': data['method'], 'params': data['params'] if 'params' in data else {} } logger.debug("Outgoing Snapcast message") logger.debug(request) # Convert to string. For some really nuts reason we need an extra trailing curly brace... request = json.dumps(request)+'}' # Attempt to send the request try: self.sock.send(request.encode('ascii')+b"\n") except socket.error, e: logger.error("Iris could not send request to Snapcast: %s" % e) socket_response = { 'error': { 'message': "Failed to send request to Snapcast", 'data': str(e) } } # Wait for response while True: try: response = self.sock.recv(8192) except socket.error, e: logger.error("Iris failed to receive Snapcast response: %s" % e) socket_response = { 'error': { 'message': "Failed to receive Snapcast response", 'data': str(e) } } if not len(response): break try: response = json.loads(response) logger.debug("Incoming Snapcast message") logger.debug(response) if 'result' in response: socket_response = response['result'] else: socket_response = response['error'] except: logger.error("Iris received malformed Snapcast response: "+response) socket_response = { 'error': { 'message': "Malformed Snapcast response", 'data': response } } return socket_response ## # Generate a random string # # Used for connection_ids where none is provided by client # @return string ## def generateGuid(self): length = 12 return ''.join(random.choice(string.lowercase) for i in range(length))