· 6 years ago · Dec 14, 2019, 09:20 PM
1# pylint: disable=W0511,C0412
2"""
3Support for LinkPlay based devices.
4
5For more details about this platform, please refer to the documentation at
6https://home-assistant.io/components/media_player.linkplay/
7"""
8
9import binascii
10import json
11import logging
12import os
13import tempfile
14import urllib.request as request2
15import xml.etree.ElementTree as ET
16
17import homeassistant.helpers.config_validation as cv
18import requests
19import voluptuous as vol
20from homeassistant.components.media_player import (MediaPlayerDevice)
21from homeassistant.components.media_player.const import (
22 DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
23 SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
24 SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET,
25 SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP)
26from homeassistant.const import (
27 ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, STATE_PAUSED, STATE_PLAYING,
28 STATE_UNKNOWN)
29from homeassistant.util.dt import utcnow
30
31from . import VERSION, ISSUE_URL, DATA_LINKPLAY
32
33_LOGGER = logging.getLogger(__name__)
34ATTR_MASTER = 'master_id'
35ATTR_PRESET = 'preset'
36ATTR_SLAVES = 'slave_ids'
37
38CONF_DEVICE_NAME = 'device_name'
39CONF_LASTFM_API_KEY = 'lastfm_api_key'
40#
41CONF_DEVICENAME_DEPRECATED = 'devicename' # TODO: Remove this deprecated key in version 3.0
42
43DEFAULT_NAME = 'LinkPlay device'
44
45LASTFM_API_BASE = "http://ws.audioscrobbler.com/2.0/?method="
46
47LINKPLAY_CONNECT_MULTIROOM_SCHEMA = vol.Schema({
48 vol.Required(ATTR_ENTITY_ID): cv.entity_id,
49 vol.Required(ATTR_MASTER): cv.entity_id
50})
51LINKPLAY_PRESET_BUTTON_SCHEMA = vol.Schema({
52 vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
53 vol.Required(ATTR_PRESET): cv.positive_int
54})
55LINKPLAY_REMOVE_SLAVES_SCHEMA = vol.Schema({
56 vol.Required(ATTR_ENTITY_ID): cv.entity_id,
57 vol.Required(ATTR_SLAVES): cv.entity_ids
58})
59
60MAX_VOL = 100
61
62
63def check_device_name_keys(conf): # TODO: Remove this check in version 3.0
64 """Ensure CONF_DEVICE_NAME or CONF_DEVICENAME_DEPRECATED are provided."""
65 if sum(param in conf for param in
66 [CONF_DEVICE_NAME, CONF_DEVICENAME_DEPRECATED]) != 1:
67 raise vol.Invalid(CONF_DEVICE_NAME + ' key not provided')
68 # if CONF_DEVICENAME_DEPRECATED in conf: # TODO: Uncomment block in version 2.0
69 # _LOGGER.warning("Key %s is deprecated. Please replace it with key %s",
70 # CONF_DEVICENAME_DEPRECATED, CONF_DEVICE_NAME)
71 return conf
72
73
74PLATFORM_SCHEMA = vol.All(cv.PLATFORM_SCHEMA.extend({
75 vol.Required(CONF_HOST): cv.string,
76 vol.Optional(CONF_DEVICE_NAME): cv.string, # TODO: Mark required in version 3.0
77 vol.Optional(CONF_NAME): cv.string,
78 vol.Optional(CONF_LASTFM_API_KEY): cv.string,
79 #
80 vol.Optional(CONF_DEVICENAME_DEPRECATED): cv.string
81}), check_device_name_keys)
82
83SERVICE_CONNECT_MULTIROOM = 'linkplay_connect_multiroom'
84SERVICE_PRESET_BUTTON = 'linkplay_preset_button'
85SERVICE_REMOVE_SLAVES = 'linkplay_remove_slaves'
86
87SERVICE_TO_METHOD = {
88 SERVICE_CONNECT_MULTIROOM: {
89 'method': 'connect_multiroom',
90 'schema': LINKPLAY_CONNECT_MULTIROOM_SCHEMA},
91 SERVICE_PRESET_BUTTON: {
92 'method': 'preset_button',
93 'schema': LINKPLAY_PRESET_BUTTON_SCHEMA},
94 SERVICE_REMOVE_SLAVES: {
95 'method': 'remove_slaves',
96 'schema': LINKPLAY_REMOVE_SLAVES_SCHEMA}
97}
98
99SUPPORT_LINKPLAY = \
100 SUPPORT_SELECT_SOURCE | SUPPORT_SELECT_SOUND_MODE | SUPPORT_SHUFFLE_SET | \
101 SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
102 SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_PLAY | \
103 SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | SUPPORT_SEEK | SUPPORT_PLAY_MEDIA
104
105SOUND_MODES = {'0': 'Normal', '1': 'Classic', '2': 'Pop', '3': 'Jazz',
106 '4': 'Vocal'}
107SOURCES = {'wifi': 'WiFi', 'line-in': 'Line-in', 'bluetooth': 'Bluetooth',
108 'optical': 'Optical', 'udisk': 'MicroSD'}
109SOURCES_MAP = {'0': 'WiFi', '10': 'WiFi', '31': 'WiFi', '40': 'Line-in',
110 '41': 'Bluetooth', '43': 'Optical'}
111UPNP_TIMEOUT = 5
112
113
114# pylint: disable=W0613
115def setup_platform(hass, config, add_entities, discovery_info=None):
116 """Set up the LinkPlay device."""
117 # Print startup message
118 _LOGGER.debug('Version %s', VERSION)
119 _LOGGER.info('If you have any issues with this you need to open an issue '
120 'here: %s', ISSUE_URL)
121
122 if DATA_LINKPLAY not in hass.data:
123 hass.data[DATA_LINKPLAY] = {}
124
125 def _service_handler(service):
126 """Map services to method of Linkplay devices."""
127 method = SERVICE_TO_METHOD.get(service.service)
128 if not method:
129 return
130
131 params = {key: value for key, value in service.data.items()
132 if key != ATTR_ENTITY_ID}
133 entity_ids = service.data.get(ATTR_ENTITY_ID)
134 if entity_ids:
135 target_players = [player for player in
136 hass.data[DATA_LINKPLAY].values()
137 if player.entity_id in entity_ids]
138 else:
139 target_players = None
140
141 for player in target_players:
142 getattr(player, method['method'])(**params)
143
144 for service in SERVICE_TO_METHOD:
145 schema = SERVICE_TO_METHOD[service]['schema']
146 hass.services.register(
147 DOMAIN, service, _service_handler, schema=schema)
148
149 dev_name = config.get(CONF_DEVICE_NAME,
150 config.get(CONF_DEVICENAME_DEPRECATED))
151 linkplay = LinkPlayDevice(config.get(CONF_HOST),
152 dev_name,
153 config.get(CONF_NAME),
154 config.get(CONF_LASTFM_API_KEY))
155
156 add_entities([linkplay])
157 hass.data[DATA_LINKPLAY][dev_name] = linkplay
158
159
160# pylint: disable=R0902,R0904
161class LinkPlayDevice(MediaPlayerDevice):
162 """Representation of a LinkPlay device."""
163
164 def __init__(self, host, devicename, name=None, lfm_api_key=None):
165 """Initialize the LinkPlay device."""
166 self._devicename = devicename
167 if name is not None:
168 self._name = name
169 else:
170 self._name = self._devicename
171 self._host = "192.168.1.207"
172 self._state = STATE_UNKNOWN
173 self._volume = 0
174 self._source = None
175 self._source_list = SOURCES.copy()
176 self._sound_mode = None
177 self._muted = False
178 self._seek_position = 0
179 self._duration = 0
180 self._position_updated_at = None
181 self._shuffle = False
182 self._media_album = None
183 self._media_artist = None
184 self._media_title = None
185 self._lpapi = LinkPlayRestData("192.168.1.207")
186 self._media_image_url = None
187 self._media_uri = None
188 self._first_update = True
189 if lfm_api_key is not None:
190 self._lfmapi = LastFMRestData(lfm_api_key)
191 else:
192 self._lfmapi = None
193 self._upnp_device = None
194 self._slave_mode = False
195 self._slave_ip = None
196 self._master = None
197 self._wifi_channel = None
198 self._ssid = None
199 self._playing_spotify = None
200 self._slave_list = None
201 self._new_song = True
202
203 @property
204 def name(self):
205 """Return the name of the device."""
206 return self._name
207
208 @property
209 def state(self):
210 """Return the state of the device."""
211 return self._state
212
213 @property
214 def volume_level(self):
215 """Volume level of the media player (0..1)."""
216 return int(self._volume) / MAX_VOL
217
218 @property
219 def is_volume_muted(self):
220 """Return boolean if volume is currently muted."""
221 return bool(int(self._muted))
222
223 @property
224 def source(self):
225 """Return the current input source."""
226 return self._source
227
228 @property
229 def source_list(self):
230 """Return the list of available input sources."""
231 return sorted(list(self._source_list.values()))
232
233 @property
234 def sound_mode(self):
235 """Return the current sound mode."""
236 return self._sound_mode
237
238 @property
239 def sound_mode_list(self):
240 """Return the available sound modes."""
241 return sorted(list(SOUND_MODES.values()))
242
243 @property
244 def supported_features(self):
245 """Flag media player features that are supported."""
246 return SUPPORT_LINKPLAY
247
248 @property
249 def media_position(self):
250 """Time in seconds of current seek position."""
251 return self._seek_position
252
253 @property
254 def media_duration(self):
255 """Time in seconds of current song duration."""
256 return self._duration
257
258 @property
259 def media_position_updated_at(self):
260 """When the seek position was last updated."""
261 return self._position_updated_at
262
263 @property
264 def shuffle(self):
265 """Return True if shuffle mode is enabled."""
266 return self._shuffle
267
268 @property
269 def media_title(self):
270 """Return title of the current track."""
271 return self._media_title
272
273 @property
274 def media_artist(self):
275 """Return name of the current track artist."""
276 return self._media_artist
277
278 @property
279 def media_album_name(self):
280 """Return name of the current track album."""
281 return self._media_album
282
283 @property
284 def media_image_url(self):
285 """Return name the image for the current track."""
286 return self._media_image_url
287
288 @property
289 def media_content_type(self):
290 """Content type of current playing media."""
291 return MEDIA_TYPE_MUSIC
292
293 @property
294 def ssid(self):
295 """SSID to use for multiroom configuration."""
296 return self._ssid
297
298 @property
299 def wifi_channel(self):
300 """Wifi channel to use for multiroom configuration."""
301 return self._wifi_channel
302
303 @property
304 def slave_ip(self):
305 """Ip used in multiroom configuration."""
306 return self._slave_ip
307
308 @property
309 def lpapi(self):
310 """Device API."""
311 return self._lpapi
312
313 def turn_on(self):
314 """Turn the media player on."""
315 _LOGGER.warning("This device cannot be turned on remotely.")
316
317 def turn_off(self):
318 """Turn off media player."""
319 self._lpapi.call('GET', 'setShutdown:0')
320 value = self._lpapi.data
321 if value != "OK":
322 _LOGGER.warning("Failed to power off the device. Got response: %s",
323 value)
324
325 def set_volume_level(self, volume):
326 """Set volume level, range 0..1."""
327 volume = str(round(volume * MAX_VOL))
328 if not self._slave_mode:
329 self._lpapi.call('GET', 'setPlayerCmd:vol:{0}'.format(str(volume)))
330 value = self._lpapi.data
331 if value == "OK":
332 self._volume = volume
333 else:
334 _LOGGER.warning("Failed to set volume. Got response: %s",
335 value)
336 else:
337 self._master.lpapi.call('GET',
338 'multiroom:SlaveVolume:{0}:{1}'.format(
339 self._slave_ip, str(volume)))
340 value = self._master.lpapi.data
341 if value == "OK":
342 self._volume = volume
343 else:
344 _LOGGER.warning("Failed to set volume. Got response: %s",
345 value)
346
347 def mute_volume(self, mute):
348 """Mute (true) or unmute (false) media player."""
349 if not self._slave_mode:
350 self._lpapi.call('GET',
351 'setPlayerCmd:mute:{0}'.format(str(int(mute))))
352 value = self._lpapi.data
353 if value == "OK":
354 self._muted = mute
355 else:
356 _LOGGER.warning("Failed mute/unmute volume. Got response: %s",
357 value)
358 else:
359 self._master.lpapi.call('GET',
360 'multiroom:SlaveMute:{0}:{1}'.format(
361 self._slave_ip, str(int(mute))))
362 value = self._master.lpapi.data
363 if value == "OK":
364 self._muted = mute
365 else:
366 _LOGGER.warning("Failed mute/unmute volume. Got response: %s",
367 value)
368
369 def media_play(self):
370 """Send play command."""
371 if not self._slave_mode:
372 self._lpapi.call('GET', 'setPlayerCmd:play')
373 value = self._lpapi.data
374 if value == "OK":
375 self._state = STATE_PLAYING
376 for slave in self._slave_list:
377 slave.set_state(STATE_PLAYING)
378 else:
379 _LOGGER.warning("Failed to start playback. Got response: %s",
380 value)
381 else:
382 self._master.media_play()
383
384 def media_pause(self):
385 """Send pause command."""
386 if not self._slave_mode:
387 self._lpapi.call('GET', 'setPlayerCmd:pause')
388 value = self._lpapi.data
389 if value == "OK":
390 self._state = STATE_PAUSED
391 for slave in self._slave_list:
392 slave.set_state(STATE_PAUSED)
393 else:
394 _LOGGER.warning("Failed to pause playback. Got response: %s",
395 value)
396 else:
397 self._master.media_pause()
398
399 def media_stop(self):
400 """Send stop command."""
401 self.media_pause()
402
403 def media_next_track(self):
404 """Send next track command."""
405 if not self._slave_mode:
406 self._lpapi.call('GET', 'setPlayerCmd:next')
407 value = self._lpapi.data
408 if value != "OK":
409 _LOGGER.warning("Failed skip to next track. Got response: %s",
410 value)
411 else:
412 self._master.media_next_track()
413
414 def media_previous_track(self):
415 """Send previous track command."""
416 if not self._slave_mode:
417 self._lpapi.call('GET', 'setPlayerCmd:prev')
418 value = self._lpapi.data
419 if value != "OK":
420 _LOGGER.warning("Failed to skip to previous track."
421 " Got response: %s", value)
422 else:
423 self._master.media_previous_track()
424
425 def media_seek(self, position):
426 """Send media_seek command to media player."""
427 if not self._slave_mode:
428 self._lpapi.call('GET',
429 'setPlayerCmd:seek:{0}'.format(str(position)))
430 value = self._lpapi.data
431 if value != "OK":
432 _LOGGER.warning("Failed to seek. Got response: %s",
433 value)
434 else:
435 self._master.media_seek(position)
436
437 def clear_playlist(self):
438 """Clear players playlist."""
439 pass
440
441 def play_media(self, media_type, media_id, **kwargs):
442 """Play media from a URL or file."""
443 if not self._slave_mode:
444 if not media_type == MEDIA_TYPE_MUSIC:
445 _LOGGER.error(
446 "Invalid media type %s. Only %s is supported",
447 media_type, MEDIA_TYPE_MUSIC)
448 return
449 self._lpapi.call('GET', 'setPlayerCmd:play:{0}'.format(media_id))
450 value = self._lpapi.data
451 if value != "OK":
452 _LOGGER.warning("Failed to play media. Got response: %s",
453 value)
454 else:
455 self._master.play_media(media_type, media_id)
456
457 def select_source(self, source):
458 """Select input source."""
459 if not self._slave_mode:
460 if source == 'MicroSD':
461 temp_source = 'udisk'
462 else:
463 temp_source = source.lower()
464 self._lpapi.call('GET',
465 'setPlayerCmd:switchmode:{0}'.format(temp_source))
466 value = self._lpapi.data
467 if value == "OK":
468 self._source = source
469 for slave in self._slave_list:
470 slave.set_source(source)
471 else:
472 _LOGGER.warning("Failed to select source. Got response: %s",
473 value)
474 else:
475 self._master.select_source(source)
476
477 def select_sound_mode(self, sound_mode):
478 """Set Sound Mode for device."""
479 if not self._slave_mode:
480 mode = list(SOUND_MODES.keys())[list(
481 SOUND_MODES.values()).index(sound_mode)]
482 self._lpapi.call('GET', 'setPlayerCmd:equalizer:{0}'.format(mode))
483 value = self._lpapi.data
484 if value == "OK":
485 self._sound_mode = sound_mode
486 for slave in self._slave_list:
487 slave.set_sound_mode(sound_mode)
488 else:
489 _LOGGER.warning("Failed to set sound mode. Got response: %s",
490 value)
491 else:
492 self._master.select_sound_mode(sound_mode)
493
494 def set_shuffle(self, shuffle):
495 """Change the shuffle mode."""
496 if not self._slave_mode:
497 mode = '2' if shuffle else '0'
498 self._lpapi.call('GET', 'setPlayerCmd:loopmode:{0}'.format(mode))
499 value = self._lpapi.data
500 if value != "OK":
501 _LOGGER.warning("Failed to change shuffle mode. "
502 "Got response: %s", value)
503 else:
504 self._master.set_shuffle(shuffle)
505
506 def preset_button(self, preset):
507 """Simulate pressing a physical preset button."""
508 if not self._slave_mode:
509 self._lpapi.call('GET',
510 'IOSimuKeyIn:{0}'.format(str(preset).zfill(3)))
511 value = self._lpapi.data
512 if value != "OK":
513 _LOGGER.warning("Failed to press preset button %s. "
514 "Got response: %s", preset, value)
515 else:
516 self._master.preset_button(preset)
517
518 def connect_multiroom(self, master_id):
519 """Add selected slaves to multiroom configuration."""
520 for device in self.hass.data[DATA_LINKPLAY].values():
521 if device.entity_id == master_id:
522 cmd = "ConnectMasterAp:ssid={0}:ch={1}:auth=OPEN:".format(
523 device.ssid, device.wifi_channel) + \
524 "encry=NONE:pwd=:chext=0"
525 self._lpapi.call('GET', cmd)
526 value = self._lpapi.data
527 if value == "OK":
528 self._slave_mode = True
529 self._master = device
530 else:
531 _LOGGER.warning("Failed to connect multiroom. "
532 "Got response: %s", value)
533
534 def remove_slaves(self, slave_ids):
535 """Remove selected slaves from multiroom configuration."""
536 for slave_id in slave_ids:
537 for device in self.hass.data[DATA_LINKPLAY].values():
538 if device.entity_id == slave_id:
539 self._lpapi.call('GET',
540 'multiroom:SlaveKickout:{0}'.format(
541 device.slave_ip))
542 value = self._lpapi.data
543 if value == "OK":
544 device.set_slave_mode(False)
545 device.set_slave_ip(None)
546 device.set_master(None)
547 else:
548 _LOGGER.warning("Failed to remove slave %s. "
549 "Got response: %s", slave_id, value)
550
551 def set_master(self, master):
552 """Set master device for multiroom configuration."""
553 self._master = master
554
555 def set_slave_mode(self, slave_mode):
556 """Set current device as slave in a multiroom configuration."""
557 self._slave_mode = slave_mode
558
559 def set_media_title(self, title):
560 """Set the media title property."""
561 self._media_title = title
562
563 def set_media_artist(self, artist):
564 """Set the media artist property."""
565 self._media_artist = artist
566
567 def set_volume(self, volume):
568 """Set the volume property."""
569 self._volume = volume
570
571 def set_muted(self, mute):
572 """Set the muted property."""
573 self._muted = mute
574
575 def set_state(self, state):
576 """Set the state property."""
577 self._state = state
578
579 def set_slave_ip(self, slave_ip):
580 """Set the slave ip property."""
581 self._slave_ip = slave_ip
582
583 def set_seek_position(self, position):
584 """Set the seek position property."""
585 self._seek_position = position
586
587 def set_duration(self, duration):
588 """Set the duration property."""
589 self._duration = duration
590
591 def set_position_updated_at(self, time):
592 """Set the position updated at property."""
593 self._position_updated_at = time
594
595 def set_source(self, source):
596 """Set the source property."""
597 self._source = source
598
599 def set_sound_mode(self, mode):
600 """Set the sound mode property."""
601 self._sound_mode = mode
602
603 def _is_playing_new_track(self, status):
604 """Check if track is changed since last update."""
605 if int(int(status['totlen']) / 1000) != self._duration:
606 return True
607 if status['totlen'] == '0':
608 # Special case when listening to radio
609 try:
610 return bool(bytes.fromhex(
611 status['Title']).decode('utf-8') != self._media_title)
612 except ValueError:
613 return True
614 return False
615
616 def _update_via_upnp(self):
617 """Update track info via UPNP."""
618 import validators
619
620 self._media_title = None
621 self._media_album = None
622 self._media_image_url = None
623
624 if self._upnp_device is None:
625 return
626
627 media_info = self._upnp_device.AVTransport.GetMediaInfo(InstanceID=0)
628 media_info = media_info.get('CurrentURIMetaData')
629
630 if media_info is None:
631 return
632
633 xml_tree = ET.fromstring(media_info)
634
635 xml_path = "{urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/}item/"
636 title_xml_path = "{http://purl.org/dc/elements/1.1/}title"
637 artist_xml_path = "{urn:schemas-upnp-org:metadata-1-0/upnp/}artist"
638 album_xml_path = "{urn:schemas-upnp-org:metadata-1-0/upnp/}album"
639 image_xml_path = "{urn:schemas-upnp-org:metadata-1-0/upnp/}albumArtURI"
640
641 self._media_title = \
642 xml_tree.find("{0}{1}".format(xml_path, title_xml_path)).text
643 self._media_artist = \
644 xml_tree.find("{0}{1}".format(xml_path, artist_xml_path)).text
645 self._media_album = \
646 xml_tree.find("{0}{1}".format(xml_path, album_xml_path)).text
647 self._media_image_url = \
648 xml_tree.find("{0}{1}".format(xml_path, image_xml_path)).text
649
650 if not validators.url(self._media_image_url):
651 self._media_image_url = None
652
653 def _update_from_id3(self):
654 """Update track info with eyed3."""
655 import eyed3
656 from urllib.error import URLError
657 try:
658 filename, _ = urllib.request.urlretrieve(self._media_uri)
659 audiofile = eyed3.load(filename)
660 self._media_title = audiofile.tag.title
661 self._media_artist = audiofile.tag.artist
662 self._media_album = audiofile.tag.album
663 # Remove tempfile when done
664 if filename.startswith(tempfile.gettempdir()):
665 os.remove(filename)
666
667 except (URLError, ValueError):
668 self._media_title = None
669 self._media_artist = None
670 self._media_album = None
671
672 def _get_lastfm_coverart(self):
673 """Get cover art from last.fm."""
674 self._lfmapi.call('GET',
675 'track.getInfo',
676 "artist={0}&track={1}".format(
677 self._media_artist,
678 self._media_title))
679 lfmdata = json.loads(self._lfmapi.data)
680 try:
681 self._media_image_url = \
682 lfmdata['track']['album']['image'][2]['#text']
683 except (ValueError, KeyError):
684 self._media_image_url = None
685
686 # pylint: disable=R0912,R0915
687 def update(self):
688 """Get the latest player details from the device."""
689 import upnpclient
690 from netdisco.ssdp import scan
691
692 if self._slave_mode:
693 return True
694
695 if self._upnp_device is None:
696 for entry in scan(UPNP_TIMEOUT):
697 try:
698 if upnpclient.Device(entry.location).friendly_name == \
699 self._devicename:
700 self._upnp_device = upnpclient.Device(entry.location)
701 break
702 except (requests.exceptions.HTTPError,
703 requests.exceptions.MissingSchema):
704 pass
705
706 self._lpapi.call('GET', 'getPlayerStatus')
707 player_api_result = self._lpapi.data
708
709 if player_api_result is None:
710 _LOGGER.warning('Unable to connect to device')
711 self._media_title = 'Unable to connect to device'
712 return True
713
714 try:
715 player_status = json.loads(player_api_result)
716 except ValueError:
717 _LOGGER.warning("REST result could not be parsed as JSON")
718 _LOGGER.debug("Erroneous JSON: %s", player_api_result)
719 player_status = None
720
721 if isinstance(player_status, dict):
722 self._lpapi.call('GET', 'getStatus')
723 device_api_result = self._lpapi.data
724 try:
725 device_status = json.loads(device_api_result)
726 except ValueError:
727 _LOGGER.warning("REST result could not be parsed as JSON")
728 _LOGGER.debug("Erroneous JSON: %s", device_api_result)
729 device_status = None
730
731 if isinstance(device_status, dict):
732 self._wifi_channel = device_status['WifiChannel']
733 self._ssid = \
734 binascii.hexlify(device_status['ssid'].encode('utf-8'))
735 self._ssid = self._ssid.decode()
736
737 # Update variables that changes during playback of a track.
738 self._volume = player_status['vol']
739 self._muted = player_status['mute']
740 self._seek_position = int(int(player_status['curpos']) / 1000)
741 self._position_updated_at = utcnow()
742 try:
743 self._media_uri = str(bytearray.fromhex(
744 player_status['iuri']).decode())
745 except KeyError:
746 self._media_uri = None
747 self._state = {
748 'stop': STATE_PAUSED,
749 'play': STATE_PLAYING,
750 'pause': STATE_PAUSED,
751 }.get(player_status['status'], STATE_UNKNOWN)
752 self._source = SOURCES_MAP.get(player_status['mode'],
753 'WiFi')
754 self._sound_mode = SOUND_MODES.get(player_status['eq'])
755 self._shuffle = (player_status['loop'] == '2')
756 self._playing_spotify = bool(player_status['mode'] == '31')
757
758 self._new_song = self._is_playing_new_track(player_status)
759 if self._playing_spotify or player_status['totlen'] == '0':
760 self._update_via_upnp()
761
762 elif self._media_uri is not None and self._new_song:
763 self._update_from_id3()
764 if self._lfmapi is not None and \
765 self._media_title is not None:
766 self._get_lastfm_coverart()
767 else:
768 self._media_image_url = None
769
770 self._duration = int(int(player_status['totlen']) / 1000)
771
772 else:
773 _LOGGER.warning("JSON result was not a dictionary")
774
775 # Get multiroom slave information
776 self._lpapi.call('GET', 'multiroom:getSlaveList')
777 slave_list = self._lpapi.data
778
779 try:
780 slave_list = json.loads(slave_list)
781 except ValueError:
782 _LOGGER.warning("REST result could not be parsed as JSON")
783 _LOGGER.debug("Erroneous JSON: %s", slave_list)
784 slave_list = None
785
786 self._slave_list = []
787 if isinstance(slave_list, dict):
788 if int(slave_list['slaves']) > 0:
789 for slave in slave_list['slave_list']:
790 device = self.hass.data[DATA_LINKPLAY].get(slave['name'])
791 if device:
792 self._slave_list.append(device)
793 device.set_master(self)
794 device.set_slave_mode(True)
795 device.set_media_title("Slave mode")
796 device.set_media_artist(self.name)
797 device.set_volume(slave['volume'])
798 device.set_muted(slave['mute'])
799 device.set_state(self.state)
800 device.set_slave_ip(slave['ip'])
801 device.set_seek_position(self.media_position)
802 device.set_duration(self.media_duration)
803 device.set_position_updated_at(
804 self.media_position_updated_at)
805 device.set_source(self._source)
806 device.set_sound_mode(self._sound_mode)
807 else:
808 _LOGGER.warning("JSON result was not a dictionary")
809
810 return True
811
812
813# pylint: disable=R0903
814class LinkPlayRestData:
815 """Class for handling the data retrieval from the LinkPlay device."""
816
817 def __init__(self, host):
818 """Initialize the data object."""
819 self.data = None
820 self._request = None
821 self._host = "192.168.1.207"
822
823 def call(self, method, cmd):
824 """Get the latest data from REST service."""
825 _LOGGER.debug(self._host)
826 self.data = None
827 self._request = None
828 resource = "http://{0}/httpapi.asp?command={1}".format("192.168.1.207", cmd)
829 _LOGGER.debug("Outputing from resource %s", resource)
830 self._request = requests.Request(method, resource).prepare()
831
832 _LOGGER.debug("Updating from %s", self._request.url)
833 _LOGGER.debug("Outputing from self._request %s", self._request)
834 try:
835 with requests.Session() as sess:
836 response = sess.send(
837 self._request, timeout=2)
838 self.data = response.text
839
840 except requests.exceptions.RequestException as ex:
841 _LOGGER.error("Error fetching data: %s from %s failed with %s",
842 self._request, self._request.url, ex)
843 self.data = None
844
845
846# pylint: disable=R0903
847class LastFMRestData:
848 """Class for handling the data retrieval from the LinkPlay device."""
849
850 def __init__(self, api_key):
851 """Initialize the data object."""
852 self.data = None
853 self._request = None
854 self._api_key = api_key
855
856 def call(self, method, cmd, params):
857 """Get the latest data from REST service."""
858 self.data = None
859 self._request = None
860 resource = "{0}{1}&{2}&api_key={3}&format=json".format(
861 LASTFM_API_BASE, cmd, params, self._api_key)
862 self._request = requests.Request(method, resource).prepare()
863 _LOGGER.debug("Updating from %s", self._request.url)
864
865 try:
866 with requests.Session() as sess:
867 response = sess.send(
868 self._request, timeout=10)
869 self.data = response.text
870
871 except requests.exceptions.RequestException as ex:
872 _LOGGER.error("Error fetching data: %s from %s failed with %s",
873 self._request, self._request.url, ex)
874 self.data = None