import random
import sys
import socket
-from utils import run_periodic, LOGGER, parse_track_title, try_parse_int, get_ip
+from utils import run_periodic, LOGGER, parse_track_title, try_parse_int, get_ip, get_hostname
from models import PlayerProvider, MusicPlayer, PlayerState, MediaType, TrackQuality, AlbumType, Artist, Album, Track, Playlist
from constants import CONF_ENABLED
(CONF_ENABLED, True, CONF_ENABLED)
]
+
class PyLMSServer(PlayerProvider):
''' Python implementation of SlimProto server '''
# 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)
+ mass.event_loop.create_task(self.start_discovery())
### Provider specific implementation #####
+ async def start_discovery(self):
+ transport, protocol = await self.mass.event_loop.create_datagram_endpoint(
+ lambda: DiscoveryProtocol(self.mass.web._http_port),
+ local_addr=('0.0.0.0', 3483))
+ try:
+ while True:
+ await asyncio.sleep(60) # serve forever
+ finally:
+ transport.close()
+
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':
async def __queue_play(self, player_id, index, send_flush=False):
''' send play command to player '''
+ if not player_id in self._player_queue:
+ return
if index == None:
index = self._player_queue_index[player_id]
- if len(self._player_queue[player_id]) >= index-1:
+ if len(self._player_queue[player_id]) >= index:
track = self._player_queue[player_id][index]
if send_flush:
self._lmsplayers[player_id].flush()
player.volume_level = lms_player.volume_level
player.cur_item_time = lms_player._elapsed_seconds
if event == "disconnected":
- player.enabled = False
+ return await self.mass.player.remove_player(player_id)
elif event == "power":
player.powered = event_data
elif event == "state":
lms_player.dataReceived(data)
else:
break
- except RuntimeError:
- LOGGER.warning("connection lost")
+ except Exception as exc:
+ # connection lost ?
+ LOGGER.warning(exc)
# disconnect
heartbeat_task.cancel()
asyncio.create_task(self.__handle_player_event(lms_player.player_id, 'disconnected'))
self.send_frame(b"strm", data)
def flush(self):
- data = self.pack_stream(b"f", autostart=b"1", flags=0)
+ data = self.pack_stream(b"f", autostart=b"0", flags=0)
self.send_frame(b"strm", data)
def pause(self):
LOGGER.info("UREQ received")
+
+# from http://wiki.slimdevices.com/index.php/SlimProtoTCPProtocol#HELO
+devices = {
+ 2: 'squeezebox',
+ 3: 'softsqueeze',
+ 4: 'squeezebox2',
+ 5: 'transporter',
+ 6: 'softsqueeze3',
+ 7: 'receiver',
+ 8: 'squeezeslave',
+ 9: 'controller',
+ 10: 'boom',
+ 11: 'softboom',
+ 12: 'squeezeplay',
+ }
+
+
+class PyLMSVolume(object):
+
+ """ Represents a sound volume. This is an awful lot more complex than it
+ sounds. """
+
+ minimum = 0
+ maximum = 100
+ step = 1
+
+ # this map is taken from Slim::Player::Squeezebox2 in the squeezecenter source
+ # i don't know how much magic it contains, or any way I can test it
+ old_map = [
+ 0, 1, 1, 1, 2, 2, 2, 3, 3, 4,
+ 5, 5, 6, 6, 7, 8, 9, 9, 10, 11,
+ 12, 13, 14, 15, 16, 16, 17, 18, 19, 20,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30, 32,
+ 33, 34, 35, 37, 38, 39, 40, 42, 43, 44,
+ 46, 47, 48, 50, 51, 53, 54, 56, 57, 59,
+ 60, 61, 63, 65, 66, 68, 69, 71, 72, 74,
+ 75, 77, 79, 80, 82, 84, 85, 87, 89, 90,
+ 92, 94, 96, 97, 99, 101, 103, 104, 106, 108, 110,
+ 112, 113, 115, 117, 119, 121, 123, 125, 127, 128
+ ];
+
+ # new gain parameters, from the same place
+ total_volume_range = -50 # dB
+ step_point = -1 # Number of steps, up from the bottom, where a 2nd volume ramp kicks in.
+ step_fraction = 1 # fraction of totalVolumeRange where alternate volume ramp kicks in.
+
+ def __init__(self):
+ self.volume = 50
+
+ def increment(self):
+ """ Increment the volume """
+ self.volume += self.step
+ if self.volume > self.maximum:
+ self.volume = self.maximum
+
+ def decrement(self):
+ """ Decrement the volume """
+ self.volume -= self.step
+ if self.volume < self.minimum:
+ self.volume = self.minimum
+
+ def old_gain(self):
+ """ Return the "Old" gain value as required by the squeezebox """
+ return self.old_map[self.volume]
+
+ def decibels(self):
+ """ Return the "new" gain value. """
+
+ step_db = self.total_volume_range * self.step_fraction
+ max_volume_db = 0 # different on the boom?
+
+ # Equation for a line:
+ # y = mx+b
+ # y1 = mx1+b, y2 = mx2+b.
+ # y2-y1 = m(x2 - x1)
+ # y2 = m(x2 - x1) + y1
+ slope_high = max_volume_db - step_db / (100.0 - self.step_point)
+ slope_low = step_db - self.total_volume_range / (self.step_point - 0.0)
+ x2 = self.volume
+ if (x2 > self.step_point):
+ m = slope_high
+ x1 = 100
+ y1 = max_volume_db
+ else:
+ m = slope_low
+ x1 = 0
+ y1 = self.total_volume_range
+ return m * (x2 - x1) + y1
+
+ def new_gain(self):
+ db = self.decibels()
+ floatmult = 10 ** (db/20.0)
+ # avoid rounding errors somehow
+ if -30 <= db <= 0:
+ return int(floatmult * (1 << 8) + 0.5) * (1<<8)
+ else:
+ return int((floatmult * (1<<16)) + 0.5)
+
+
+##### UDP DISCOVERY STUFF #############
+
class Datagram(object):
@classmethod
client = None
def __init__(self, data):
- s = struct.unpack('!cxBB8x6B', data)
+ s = struct.unpack('!cxBB8x6B', data.encode())
assert s[0] == 'd'
self.device = s[1]
self.firmware = hex(s[2])
def __init__(self, hostname, port):
hostname = hostname[:16].encode("UTF-8")
hostname += (16 - len(hostname)) * '\x00'
- self.packet = struct.pack('!c16s', 'D', hostname)
+ self.packet = struct.pack('!c16s', 'D', hostname).decode()
class TLVDiscoveryRequestDatagram(Datagram):
idx = 1
length = len(data)-5
while idx <= length:
- typ, l = struct.unpack_from("4sB", data, idx)
+ typ, l = struct.unpack_from("4sB", data.encode(), idx)
if l:
val = data[idx+5:idx+5+l]
idx += 5+l
else:
val = None
idx += 5
+ typ = typ.decode()
requestdata[typ] = val
self.data = requestdata
class DiscoveryProtocol():
+ def __init__(self, web_port):
+ self.web_port = web_port
+
def connection_made(self, transport):
self.transport = transport
# Allow receiving multicast broadcasts
for typ, value in requestdata.items():
if typ == 'NAME':
# send full host name - no truncation
- value = 'macbook-marcel' # TODO
+ value = get_hostname()
elif typ == 'IPAD':
# send ipaddress as a string only if it is set
- value = '192.168.1.145' # TODO
+ value = get_ip()
# :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
+ json_port = self.web_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)
+ value = 'musicassistant'
else:
- LOGGER.error('Unexpected information request: %r', typ)
+ LOGGER.debug('Unexpected information request: %r', typ)
typ = None
if typ:
responsedata[typ] = value
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))
+ LOGGER.debug("Data received from %s: %s" % (addr, dgram))
if isinstance(dgram, ClientDiscoveryDatagram):
self.sendDiscoveryResponse(addr)
elif isinstance(dgram, TLVDiscoveryRequestDatagram):
LOGGER.exception(exc)
def sendDiscoveryResponse(self, addr):
- dgram = DiscoveryResponseDatagram('macbook-marcel', 3483)
- LOGGER.info("Sending discovery response %r" % (dgram.packet,))
+ dgram = DiscoveryResponseDatagram(get_hostname(), 3483)
+ LOGGER.debug("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,))
+ LOGGER.debug("Sending discovery response %r" % (dgram.packet,))
self.transport.sendto(dgram.packet.encode(), addr)
-
-
-
-
-
-
-# from http://wiki.slimdevices.com/index.php/SlimProtoTCPProtocol#HELO
-devices = {
- 2: 'squeezebox',
- 3: 'softsqueeze',
- 4: 'squeezebox2',
- 5: 'transporter',
- 6: 'softsqueeze3',
- 7: 'receiver',
- 8: 'squeezeslave',
- 9: 'controller',
- 10: 'boom',
- 11: 'softboom',
- 12: 'squeezeplay',
- }
-
-
-class PyLMSVolume(object):
-
- """ Represents a sound volume. This is an awful lot more complex than it
- sounds. """
-
- minimum = 0
- maximum = 100
- step = 1
-
- # this map is taken from Slim::Player::Squeezebox2 in the squeezecenter source
- # i don't know how much magic it contains, or any way I can test it
- old_map = [
- 0, 1, 1, 1, 2, 2, 2, 3, 3, 4,
- 5, 5, 6, 6, 7, 8, 9, 9, 10, 11,
- 12, 13, 14, 15, 16, 16, 17, 18, 19, 20,
- 22, 23, 24, 25, 26, 27, 28, 29, 30, 32,
- 33, 34, 35, 37, 38, 39, 40, 42, 43, 44,
- 46, 47, 48, 50, 51, 53, 54, 56, 57, 59,
- 60, 61, 63, 65, 66, 68, 69, 71, 72, 74,
- 75, 77, 79, 80, 82, 84, 85, 87, 89, 90,
- 92, 94, 96, 97, 99, 101, 103, 104, 106, 108, 110,
- 112, 113, 115, 117, 119, 121, 123, 125, 127, 128
- ];
-
- # new gain parameters, from the same place
- total_volume_range = -50 # dB
- step_point = -1 # Number of steps, up from the bottom, where a 2nd volume ramp kicks in.
- step_fraction = 1 # fraction of totalVolumeRange where alternate volume ramp kicks in.
-
- def __init__(self):
- self.volume = 50
-
- def increment(self):
- """ Increment the volume """
- self.volume += self.step
- if self.volume > self.maximum:
- self.volume = self.maximum
-
- def decrement(self):
- """ Decrement the volume """
- self.volume -= self.step
- if self.volume < self.minimum:
- self.volume = self.minimum
-
- def old_gain(self):
- """ Return the "Old" gain value as required by the squeezebox """
- return self.old_map[self.volume]
-
- def decibels(self):
- """ Return the "new" gain value. """
-
- step_db = self.total_volume_range * self.step_fraction
- max_volume_db = 0 # different on the boom?
-
- # Equation for a line:
- # y = mx+b
- # y1 = mx1+b, y2 = mx2+b.
- # y2-y1 = m(x2 - x1)
- # y2 = m(x2 - x1) + y1
- slope_high = max_volume_db - step_db / (100.0 - self.step_point)
- slope_low = step_db - self.total_volume_range / (self.step_point - 0.0)
- x2 = self.volume
- if (x2 > self.step_point):
- m = slope_high
- x1 = 100
- y1 = max_volume_db
- else:
- m = slope_low
- x1 = 0
- y1 = self.total_volume_range
- return m * (x2 - x1) + y1
-
- def new_gain(self):
- db = self.decibels()
- floatmult = 10 ** (db/20.0)
- # avoid rounding errors somehow
- if -30 <= db <= 0:
- return int(floatmult * (1 << 8) + 0.5) * (1<<8)
- else:
- return int((floatmult * (1<<16)) + 0.5)
\ No newline at end of file