import asyncio
import os
import struct
+from collections import OrderedDict
import time
import decimal
from typing import List
import random
import sys
-from netaddr import EUI
-from utils import run_periodic, LOGGER, parse_track_title
+import socket
+from utils import run_periodic, LOGGER, parse_track_title, try_parse_int, get_ip
from models import PlayerProvider, MusicPlayer, PlayerState, MediaType, TrackQuality, AlbumType, Artist, Album, Track, Playlist
from constants import CONF_ENABLED
''' setup the provider'''
enabled = mass.config["playerproviders"]['pylms'].get(CONF_ENABLED)
if enabled:
- provider = PyLMSProvider(mass)
+ provider = PyLMSServer(mass)
return provider
return False
def config_entries():
''' get the config entries for this provider (list with key/value pairs)'''
return [
- (CONF_ENABLED, False, CONF_ENABLED)
+ (CONF_ENABLED, True, CONF_ENABLED)
]
-class PyLMSProvider(PlayerProvider):
- ''' Python implementation of SlimProto '''
+class PyLMSServer(PlayerProvider):
+ ''' Python implementation of SlimProto server '''
def __init__(self, mass):
self.prov_id = 'pylms'
self.icon = ''
self.mass = mass
self._players = {}
- self._players = {}
+ self._lmsplayers = {}
+ self._player_queue = {}
+ self._player_queue_index = {}
self.buffer = b''
self.last_msg_received = 0
self.supported_musicproviders = ['http']
- mass.event_loop.create_task(asyncio.start_server(self.__handle_client, 'localhost', 3483))
+ # start slimproto server
+ mass.event_loop.create_task(asyncio.start_server(self.__handle_socket_client, '0.0.0.0', 3483))
+ # setup discovery
+ listen = mass.event_loop.create_datagram_endpoint(
+ DiscoveryProtocol, local_addr=('0.0.0.0', 3483),
+ family=socket.AF_INET, reuse_address=True, reuse_port=True,
+ allow_broadcast=True)
+ mass.event_loop.create_task(listen)
+
+ ### Provider specific implementation #####
+
+ async def player_command(self, player_id, cmd:str, cmd_args=None):
+ ''' issue command on player (play, pause, next, previous, stop, power, volume, mute) '''
+ if cmd == 'play':
+ if self._players[player_id].state == PlayerState.Stopped:
+ await self.__queue_play(player_id, None)
+ else:
+ self._lmsplayers[player_id].unpause()
+ elif cmd == 'pause':
+ self._lmsplayers[player_id].pause()
+ elif cmd == 'stop':
+ self._lmsplayers[player_id].stop()
+ elif cmd == 'next':
+ self._lmsplayers[player_id].next()
+ elif cmd == 'previous':
+ await self.__queue_previous(player_id)
+ elif cmd == 'power' and cmd_args == 'off':
+ self._lmsplayers[player_id].power_off()
+ elif cmd == 'power':
+ self._lmsplayers[player_id].power_on()
+ elif cmd == 'volume':
+ self._lmsplayers[player_id].volume_set(try_parse_int(cmd_args))
+ elif cmd == 'mute' and cmd_args == 'off':
+ self._lmsplayers[player_id].unmute()
+ elif cmd == 'mute':
+ self._lmsplayers[player_id].mute()
+
+ async def player_queue(self, player_id, offset=0, limit=50):
+ ''' return the current items in the player's queue '''
+ return self._player_queue[player_id][offset:limit]
+
+ async def play_media(self, player_id, media_items, queue_opt='play'):
+ '''
+ play media on a player
+ '''
+ cur_queue_index = self._player_queue_index.get(player_id, 0)
+
+ if queue_opt == 'replace' or not self._player_queue[player_id]:
+ # overwrite queue with new items
+ self._player_queue[player_id] = media_items
+ await self.__queue_play(player_id, 0)
+ elif queue_opt == 'play':
+ # replace current item with new item(s)
+ self._player_queue[player_id] = self._player_queue[player_id][:cur_queue_index] + media_items + self._player_queue[player_id][cur_queue_index+1:]
+ await self.__queue_play(player_id, cur_queue_index)
+ elif queue_opt == 'next':
+ # insert new items at current index +1
+ self._player_queue[player_id] = self._player_queue[player_id][:cur_queue_index+1] + media_items + self._player_queue[player_id][cur_queue_index+1:]
+ elif queue_opt == 'add':
+ # add new items at end of queue
+ self._player_queue[player_id] = self._player_queue[player_id] + media_items
+
+ ### Provider specific (helper) methods #####
+
+ async def __queue_play(self, player_id, index):
+ ''' send play command to player '''
+ if not index:
+ index = self._player_queue_index[player_id]
+ if len(self._player_queue[player_id]) >= index-1:
+ track = self._player_queue[player_id][index]
+ self._lmsplayers[player_id].stop()
+ self._lmsplayers[player_id].play(track.uri)
+ self._player_queue_index[player_id] = index
+
+ async def __queue_next(self, player_id):
+ ''' request next track from queue '''
+ if not player_id in self._player_queue or not player_id in self._player_queue:
+ return
+ cur_queue_index = self._player_queue_index[player_id]
+ if len(self._player_queue[player_id]) > cur_queue_index:
+ new_queue_index = cur_queue_index + 1
+ elif self._players[player_id].repeat_enabled:
+ new_queue_index = 0
+ else:
+ LOGGER.warning("next track requested but no more tracks in queue")
+ return
+ return await self.__queue_play(player_id, new_queue_index)
+
+ async def __queue_previous(self, player_id):
+ ''' request previous track from queue '''
+ if not player_id in self._player_queue:
+ return
+ cur_queue_index = self._player_queue_index[player_id]
+ if cur_queue_index == 0 and len(self._player_queue[player_id]) > 1:
+ new_queue_index = len(self._player_queue[player_id]) -1
+ elif cur_queue_index == 0:
+ new_queue_index = cur_queue_index
+ else:
+ new_queue_index -= 1
+ self._player_queue_index[player_id] = new_queue_index
+ return await self.__queue_play(player_id, new_queue_index)
+
+ async def __handle_player_event(self, player_id, event, event_data=None):
+ ''' handle event from player '''
+ if not player_id:
+ return
+ LOGGER.debug("Event from player %s: %s - event_data: %s" %(player_id, event, str(event_data)))
+ lms_player = self._lmsplayers[player_id]
+ if event == "next_track":
+ return await self.__queue_next(player_id)
+ if not player_id in self._players:
+ player = MusicPlayer()
+ player.player_id = player_id
+ player.player_provider = self.prov_id
+ self._players[player_id] = player
+ if not player_id in self._player_queue:
+ self._player_queue[player_id] = []
+ if not player_id in self._player_queue_index:
+ self._player_queue_index[player_id] = 0
+ else:
+ player = self._players[player_id]
+ # update player properties
+ player.name = lms_player.player_name
+ player.volume_level = lms_player.volume_level
+ player.cur_item_time = lms_player._elapsed_seconds
+ if event == "disconnected":
+ player.enabled = False
+ elif event == "power":
+ player.powered = event_data
+ elif event == "state":
+ player.state = event_data
+ if self._player_queue[player_id]:
+ cur_queue_index = self._player_queue_index[player_id]
+ player.cur_item = self._player_queue[player_id][cur_queue_index]
+ # update player details
+ await self.mass.player.update_player(player)
+
+ async def __handle_socket_client(self, reader, writer):
+ ''' handle a client connection on the socket'''
+ LOGGER.debug("new socket client connected")
+ stream_host = get_ip()
+ stream_port = self.mass.config['base']['web']['http_port']
+ lms_player = PyLMSPlayer(stream_host, stream_port)
- async def __handle_client(self, reader, writer):
- request = None
- lms_player = PyLMSPlayer()
-
def send_frame(command, data):
+ ''' send command to lms player'''
packet = struct.pack('!H', len(data) + 4) + command + data
- print("Sending packet %r" % packet)
writer.write(packet)
+
+ def handle_event(event, event_data=None):
+ ''' handle events from player'''
+ if event == "connected":
+ self._lmsplayers[lms_player.player_id] = lms_player
+ asyncio.create_task(self.__handle_player_event(lms_player.player_id, event, event_data))
+
lms_player.send_frame = send_frame
- asyncio.create_task(self.send_play(lms_player))
+ lms_player.send_event = handle_event
+ heartbeat_task = asyncio.create_task(self.send_heartbeat(lms_player))
- while request != 'quit':
- data = await reader.read(100)
- if not data:
+ # keep reading bytes from the socket
+ while True:
+ data = await reader.read(64)
+ if data:
+ lms_player.dataReceived(data)
+ else:
break
- #data = data.decode('latin-1')
- print(data)
- lms_player.dataReceived(data)
-
- #response = str(eval(request)) + '\n'
- #writer.write(response.encode('utf8'))
- LOGGER.info('client disconnected')
-
- async def send_play(self, lms_player):
- await asyncio.sleep(5)
- lms_player.play()
- lms_player.unpause()
-
-
-
-
+ # disconnect
+ heartbeat_task.cancel()
+ asyncio.create_task(self.__handle_player_event(lms_player.player_id, 'disconnected'))
+ @run_periodic(5)
+ async def send_heartbeat(self, lms_player):
+ timestamp = int(time.time())
+ data = lms_player.pack_stream(b"t", replayGain=timestamp, flags=0)
+ lms_player.send_frame(b"strm", data)
### Provider specific implementation #####
class PyLMSPlayer(object):
- ''' Python implementation of SlimProto '''
-
- # these numbers are also in a dict in Collection. This should obviously be refactored.
- typeMap = {
- 0: b'o', # ogg
- 1: b'm', # mp3
- 2: b'f', # flac
- 3: b'p', # pcm (wav etc.)
- }
+ ''' very basic Python implementation of SlimProto '''
- def __init__(self):
+ def __init__(self, stream_host, stream_port):
self.buffer = b''
#self.display = Display()
- self.volume = PyLMSVolume()
- self.device_type = None
- self.mac_address = None
self.send_frame = None
-
- def connectionEstablished(self):
- """ Called when a connection has been successfully established with
- the player. """
- #self.service.evreactor.fireEvent(StateChanged(self, StateChanged.State.ESTABLISHED))
- LOGGER.info("Connected to squeezebox")
-
-
- def connectionLost(self, reason):
- #self.service.evreactor.fireEvent(StateChanged(self, StateChanged.State.DISCONNECTED))
- #self.service.players.remove(self)
- pass
-
+ self.send_event = None
+ self.stream_host = stream_host
+ self.stream_port = stream_port
+ self.playback_millis = 0
+ self._volume = PyLMSVolume()
+ self._device_type = None
+ self._mac_address = None
+ self._player_name = None
+ self._last_volume = 0
+ self._last_heartbeat = 0
+ self._elapsed_seconds = 0
+ self._elapsed_milliseconds = 0
+
+ @property
+ def player_name(self):
+ if self._player_name:
+ return self._player_name
+ return "%s - %s" %(self._device_type, self._mac_address)
+
+ @property
+ def player_id(self):
+ return self._mac_address
+
+ @property
+ def volume_level(self):
+ return self._volume.volume
+
def dataReceived(self, data):
self.buffer = self.buffer + data
if len(self.buffer) > 8:
if len(self.buffer) >= plen:
packet, self.buffer = self.buffer[8:plen], self.buffer[plen:]
operation = operation.strip(b"!").strip().decode()
- LOGGER.info("operation: %s" % operation)
+ #LOGGER.info("operation: %s" % operation)
handler = getattr(self, "process_%s" % operation, None)
if handler is None:
raise NotImplementedError
handler(packet)
-
-
def send_version(self):
- self.send_frame(b'vers', b'7.0')
+ self.send_frame(b'vers', b'7.8')
- def pack_stream(self, command, autostart=b"1", formatbyte = b'o', pcmargs = b'1321', threshold = 255,
+ def pack_stream(self, command, autostart=b"1", formatbyte = b'o', pcmargs = (b'?',b'?',b'?',b'?'), threshold = 200,
spdif = b'0', transDuration = 0, transType = b'0', flags = 0x40, outputThreshold = 0,
- replayGainHigh = 0, replayGainLow = 0, serverPort = 8095, serverIp = 0):
- return struct.pack("!ccc4sBcBcBBBHHHL",
- command, autostart, formatbyte, pcmargs,
+ replayGain=0, serverPort = 8095, serverIp = 0):
+ return struct.pack("!cccccccBcBcBBBLHL",
+ command, autostart, formatbyte, *pcmargs,
threshold, spdif, transDuration, transType,
- flags, outputThreshold, 0, replayGainHigh, replayGainLow, serverPort, serverIp)
+ flags, outputThreshold, 0, replayGain, serverPort, serverIp)
- def stop_streaming(self):
+ def stop(self):
data = self.pack_stream(b"q", autostart=b"0", flags=0)
self.send_frame(b"strm", data)
def pause(self):
- data = self.pack_stream(b"bp", autostart=b"0", flags=0)
+ data = self.pack_stream(b"p", autostart=b"0", flags=0)
self.send_frame(b"strm", data)
LOGGER.info("Sending pause request")
self.send_frame(b"strm", data)
LOGGER.info("Sending unpause request")
- def stop(self):
- self.stop_streaming()
+ def next(self):
+ data = self.pack_stream(b"f", autostart=b"0", flags=0)
+ self.send_frame(b"strm", data)
+ self.send_event("next_track")
+
+ def previous(self):
+ data = self.pack_stream(b"f", autostart=b"0", flags=0)
+ self.send_frame(b"strm", data)
+ self.send_event("previous_track")
- def play(self):
+ def power_on(self):
+ self.send_frame(b"aude", struct.pack("2B", 1, 1))
+ self.send_event("power", True)
+
+ def power_off(self):
+ self.stop()
+ self.send_frame(b"aude", struct.pack("2B", 0, 0))
+ self.send_event("power", False)
+
+ def mute_on(self):
+ self.send_frame(b"aude", struct.pack("2B", 0, 0))
+ self.send_event("mute", True)
+
+ def mute_off(self):
+ self.send_frame(b"aude", struct.pack("2B", 1, 1))
+ self.send_event("mute", False)
+
+ def volume_up(self):
+ self._volume.increment()
+ self.send_volume()
+
+ def volume_down(self):
+ self._volume.decrement()
+ self.send_volume()
+
+ def volume_set(self, new_vol):
+ self._volume.volume = new_vol
+ self.send_volume()
+
+ def play(self, uri, crossfade=False):
command = b's'
- autostart = b'1'
- formatbyte = self.typeMap[2]
- uri = "/stream?provider=spotify&track_id=56z8UyE4foPVnSrER7lVR5"
- data = self.pack_stream(command, autostart=autostart, flags=0x00, formatbyte=formatbyte)
- request = "GET %s HTTP/1.0\r\n\r\n" % uri
+ autostart = b'3' # we use direct stream for now so let the player do the messy work with buffers
+ transType= b'1' if crossfade else b'0'
+ transDuration = 10 if crossfade else 0
+ formatbyte = b'f' # fixed to flac
+ uri = '/stream' + uri.split('/stream')[1]
+ data = self.pack_stream(command, autostart=autostart, flags=0x00, formatbyte=formatbyte, transType=transType, transDuration=transDuration)
+ headers = "Connection: close\r\nAccept: */*\r\nHost: %s:%s\r\n" %(self.stream_host, self.stream_port)
+ request = "GET %s HTTP/1.0\r\n%s\r\n" % (uri, headers)
data = data + request.encode("utf-8")
self.send_frame(b'strm', data)
- LOGGER.info("Requesting play from squeezebox %s" % (id(self),))
- #self.displayTrack(track)
-
- # def play(self, track):
- # command = b's'
- # autostart = b'1'
- # formatbyte = self.typeMap[track.type]
- # data = self.pack_stream(command, autostart=autostart, flags=0x00, formatbyte=formatbyte)
- # request = "GET %s HTTP/1.0\r\n\r\n" % (track.player_uri(id(self)),)
- # data = data + request.encode("utf-8")
- # self.send_frame(b'strm', data)
- # LOGGER.info("Requesting play from squeezebox %s" % (id(self),))
- # self.displayTrack(track)
+ LOGGER.info("Requesting play from squeezebox" )
def displayTrack(self, track):
self.render("%s by %s" % (track.title, track.artist))
def process_HELO(self, data):
- #(devId, rev, mac, wlan, bytes) = struct.unpack('BB6sHL', data[:16])
(devId, rev, mac) = struct.unpack('BB6s', data[:8])
- (mac,) = struct.unpack(">q", b'00'+mac)
- mac = EUI(mac)
- self.device_type = devices.get(devId, 'unknown device')
- self.mac_address = str(mac)
- LOGGER.info("HELO received from %s %s" % (self.mac_address, self.device_type))
+ device_mac = ':'.join("%02x" % x for x in mac)
+ self._device_type = devices.get(devId, 'unknown device')
+ self._mac_address = str(device_mac).lower()
+ LOGGER.debug("HELO received from %s %s" % (self._mac_address, self._device_type))
self.init_client()
def init_client(self):
+ ''' initialize a new connected client '''
+ self.send_event("connected")
self.send_version()
- self.stop_streaming()
+ self.stop()
self.setBrightness()
#self.set_visualisation(SpectrumAnalyser())
self.send_frame(b"setd", struct.pack("B", 0))
self.send_frame(b"setd", struct.pack("B", 4))
- self.enableAudio()
- self.send_volume()
- self.send_frame(b"strm", self.pack_stream(b't', autostart=b"1", flags=0, replayGainHigh=0))
- self.connectionEstablished()
-
- def enableAudio(self):
- self.send_frame(b"aude", struct.pack("2B", 1, 1))
-
+ self.power_on()
+ self.volume_set(40) # TODO: remember last volume
+
def send_volume(self):
- og = self.volume.old_gain()
- ng = self.volume.new_gain()
- LOGGER.info("Volume set to %d (%d/%d)" % (self.volume.volume, og, ng))
+ og = self._volume.old_gain()
+ ng = self._volume.new_gain()
+ LOGGER.info("Volume set to %d (%d/%d)" % (self._volume.volume, og, ng))
d = self.send_frame(b"audg", struct.pack("!LLBBLL", og, og, 1, 255, ng, ng))
- #self.service.evreactor.fireEvent(VolumeChanged(self, self.volume))
- return d
+ self.send_event("volume", self._volume.volume)
def setBrightness(self, level=4):
assert 0 <= level <= 4
self.send_frame(b"grfe", frame)
def process_STAT(self, data):
- #print "STAT received: %r" % data
ev = data[:4]
if ev == b'\x00\x00\x00\x00':
LOGGER.info("Presumed informational stat message")
handler(data[4:])
def stat_aude(self, data):
- LOGGER.info("ACK aude")
+ (spdif_enable, dac_enable) = struct.unpack("2B", data[:4])
+ powered = spdif_enable or dac_enable
+ self.send_event("power", powered)
+ LOGGER.debug("ACK aude - Received player power: %s" % powered)
def stat_audg(self, data):
- LOGGER.info("ACK audg")
+ LOGGER.info("Received volume_level from player %s" % data)
+ self.send_event("volume", self._volume.volume)
def stat_strm(self, data):
- LOGGER.info("ACK strm")
+ LOGGER.debug("ACK strm")
+ #self.send_frame(b"cont", b"0")
def stat_STMc(self, data):
- LOGGER.info("Status Message: Connect")
+ LOGGER.debug("Status Message: Connect")
def stat_STMd(self, data):
- LOGGER.info("Decoder Ready")
- #self.service.evreactor.fireEvent(StateChanged(self, StateChanged.State.READY))
+ LOGGER.debug("Decoder Ready for next track")
+ self.send_event("next_track")
def stat_STMe(self, data):
LOGGER.info("Connection established")
def stat_STMf(self, data):
LOGGER.info("Status Message: Connection closed")
+ self.send_event("state", PlayerState.Stopped)
def stat_STMh(self, data):
LOGGER.info("Status Message: End of headers")
def stat_STMn(self, data):
- LOGGER.info("Decoder does not support file format")
+ LOGGER.error("Decoder does not support file format")
def stat_STMo(self, data):
- LOGGER.info("Output Underrun")
-
+ ''' No more decoded (uncompressed) data to play; triggers rebuffering. '''
+ LOGGER.debug("Output Underrun")
+
def stat_STMp(self, data):
- LOGGER.info("Pause confirmed")
- #self.service.evreactor.fireEvent(StateChanged(self, StateChanged.State.PAUSED))
+ '''Pause confirmed'''
+ self.send_event("state", PlayerState.Paused)
def stat_STMr(self, data):
- LOGGER.info("Resume confirmed")
- #self.service.evreactor.fireEvent(StateChanged(self, StateChanged.State.PLAYING))
+ '''Resume confirmed'''
+ self.send_event("state", PlayerState.Playing)
def stat_STMs(self, data):
- LOGGER.info("Player status message: playback of new track has started")
- #self.service.evreactor.fireEvent(StateChanged(self, StateChanged.State.PLAYING))
+ '''Playback of new track has started'''
+ self.send_event("state", PlayerState.Playing)
def stat_STMt(self, data):
- """ Timer heartbeat """
- self.last_heartbeat = time.time()
+ """ heartbeat from client """
+ timestamp = time.time()
+ self._last_heartbeat = timestamp
+ (num_crlf, mas_initialized, mas_mode, rptr, wptr,
+ bytes_received_h, bytes_received_l, signal_strength,
+ jiffies, output_buffer_size, output_buffer_fullness,
+ elapsed_seconds, voltage, elapsed_milliseconds,
+ server_timestamp, error_code) = struct.unpack("!BBBLLLLHLLLLHLLH", data)
+ if elapsed_seconds != self._elapsed_seconds:
+ self.send_event("progress")
+ self._elapsed_seconds = elapsed_seconds
+ self._elapsed_milliseconds = elapsed_milliseconds
def stat_STMu(self, data):
- LOGGER.info("End of playback")
- #self.service.evreactor.fireEvent(StateChanged(self, StateChanged.State.UNDERRUN))
+ '''Normal end of playback'''
+ LOGGER.info("End of playback - Underrun")
+ self.send_event("state", PlayerState.Stopped)
def process_BYE(self, data):
LOGGER.info("BYE received")
+ self.send_event("disconnected")
def process_RESP(self, data):
LOGGER.info("RESP received")
+ self.send_frame(b"cont", b"0")
def process_BODY(self, data):
LOGGER.info("BODY received")
this player will be called. This is mostly relevant for volume changes
- most other button presses will require some context to operate. """
(time, code) = struct.unpack("!IxxI", data)
- command = Remote.codes.get(code, None)
- if command is not None:
- LOGGER.info("IR received: %r, %r" % (code, command))
- #self.service.evreactor.fireEvent(RemoteButtonPressed(self, command))
- else:
- LOGGER.info("Unknown IR received: %r, %r" % (time, code))
+ LOGGER.info("IR code %s" % code)
+ # command = Remote.codes.get(code, None)
+ # if command is not None:
+ # LOGGER.info("IR received: %r, %r" % (code, command))
+ # #self.service.evreactor.fireEvent(RemoteButtonPressed(self, command))
+ # else:
+ # LOGGER.info("Unknown IR received: %r, %r" % (time, code))
def process_RAWI(self, data):
LOGGER.info("RAWI received")
LOGGER.info("BUTN received")
def process_KNOB(self, data):
+ ''' Transporter only, knob-related '''
LOGGER.info("KNOB received")
def process_SETD(self, data):
- LOGGER.info("SETD received")
+ ''' Get/set player firmware settings '''
+ LOGGER.debug("SETD received %s" % data)
+ cmd_id = data[0]
+ if cmd_id == 0:
+ # received player name
+ data = data[1:].decode()
+ self._player_name = data
+ self.send_event("name")
def process_UREQ(self, data):
LOGGER.info("UREQ received")
- def process_remote_volumeup(self):
- self.volume.increment()
- self.send_volume()
- def process_remote_volumedown(self):
- self.volume.decrement()
- self.send_volume()
+class Datagram(object):
+
+ @classmethod
+ def decode(self, data):
+ if data[0] == 'e':
+ return TLVDiscoveryRequestDatagram(data)
+ elif data[0] == 'E':
+ return TLVDiscoveryResponseDatagram(data)
+ elif data[0] == 'd':
+ return ClientDiscoveryDatagram(data)
+ elif data[0] == 'h':
+ pass # Hello!
+ elif data[0] == 'i':
+ pass # IR
+ elif data[0] == '2':
+ pass # i2c?
+ elif data[0] == 'a':
+ pass # ack!
+
+class ClientDiscoveryDatagram(Datagram):
+
+ device = None
+ firmware = None
+ client = None
+
+ def __init__(self, data):
+ s = struct.unpack('!cxBB8x6B', data)
+ assert s[0] == 'd'
+ self.device = s[1]
+ self.firmware = hex(s[2])
+ self.client = ":".join(["%02x" % (x,) for x in s[3:]])
+
+ def __repr__(self):
+ return "<%s device=%r firmware=%r client=%r>" % (self.__class__.__name__, self.device, self.firmware, self.client)
+
+class DiscoveryResponseDatagram(Datagram):
+
+ def __init__(self, hostname, port):
+ hostname = hostname[:16].encode("UTF-8")
+ hostname += (16 - len(hostname)) * '\x00'
+ self.packet = struct.pack('!c16s', 'D', hostname)
+
+class TLVDiscoveryRequestDatagram(Datagram):
+
+ def __init__(self, data):
+ requestdata = OrderedDict()
+ assert data[0] == 'e'
+ idx = 1
+ length = len(data)-5
+ while idx <= length:
+ typ, l = struct.unpack_from("4sB", data, idx)
+ if l:
+ val = data[idx+5:idx+5+l]
+ idx += 5+l
+ else:
+ val = None
+ idx += 5
+ requestdata[typ] = val
+ self.data = requestdata
+
+ def __repr__(self):
+ return "<%s data=%r>" % (self.__class__.__name__, self.data.items())
+
+class TLVDiscoveryResponseDatagram(Datagram):
+
+ def __init__(self, responsedata):
+ parts = ['E'] # new discovery format
+ for typ, value in responsedata.items():
+ if value is None:
+ value = ''
+ elif len(value) > 255:
+ LOGGER.warning("Response %s too long, truncating to 255 bytes" % typ)
+ value = value[:255]
+ parts.extend((typ, chr(len(value)), value))
+ self.packet = ''.join(parts)
+
+class DiscoveryProtocol():
+
+ def connection_made(self, transport):
+ self.transport = transport
+ # Allow receiving multicast broadcasts
+ sock = self.transport.get_extra_info('socket')
+ group = socket.inet_aton('239.255.255.250')
+ mreq = struct.pack('4sL', group, socket.INADDR_ANY)
+ sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
+
+ def build_TLV_response(self, requestdata):
+ responsedata = OrderedDict()
+ for typ, value in requestdata.items():
+ if typ == 'NAME':
+ # send full host name - no truncation
+ value = 'macbook-marcel' # TODO
+ elif typ == 'IPAD':
+ # send ipaddress as a string only if it is set
+ value = '192.168.1.145' # TODO
+ # :todo: IPv6
+ if value == '0.0.0.0':
+ # do not send back an ip address
+ typ = None
+ elif typ == 'JSON':
+ # send port as a string
+ json_port = 9000 # todo: web.service.port
+ value = str(json_port)
+ elif typ == 'VERS':
+ # send server version
+ value = '7.9'
+ elif typ == 'UUID':
+ # send server uuid
+ value = 'test'
+ # elif typ == 'JVID':
+ # # not handle, just log the information
+ # typ = None
+ # log.msg("Jive: %x:%x:%x:%x:%x:%x:" % struct.unpack('>6B', value),
+ # logLevel=logging.INFO)
+ else:
+ LOGGER.error('Unexpected information request: %r', typ)
+ typ = None
+ if typ:
+ responsedata[typ] = value
+ return responsedata
+
+ def datagram_received(self, data, addr):
+ try:
+ data = data.decode()
+ LOGGER.info('Received %r from %s' % (data, addr))
+ dgram = Datagram.decode(data)
+ LOGGER.info("Data received from %s: %s" % (addr, dgram))
+ if isinstance(dgram, ClientDiscoveryDatagram):
+ self.sendDiscoveryResponse(addr)
+ elif isinstance(dgram, TLVDiscoveryRequestDatagram):
+ resonsedata = self.build_TLV_response(dgram.data)
+ self.sendTLVDiscoveryResponse(resonsedata, addr)
+ except Exception as exc:
+ LOGGER.exception(exc)
+
+ def sendDiscoveryResponse(self, addr):
+ dgram = DiscoveryResponseDatagram('macbook-marcel', 3483)
+ LOGGER.info("Sending discovery response %r" % (dgram.packet,))
+ self.transport.sendto(dgram.packet.encode(), addr)
+
+ def sendTLVDiscoveryResponse(self, resonsedata, addr):
+ dgram = TLVDiscoveryResponseDatagram(resonsedata)
+ LOGGER.info("Sending discovery response %r" % (dgram.packet,))
+ self.transport.sendto(dgram.packet.encode(), addr)
+
+
+
+