Advertisement
Guest User

Untitled

a guest
Dec 14th, 2019
2,051
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 31.98 KB | None | 0 0
  1. # pylint: disable=W0511,C0412
  2. """
  3. Support for LinkPlay based devices.
  4.  
  5. For more details about this platform, please refer to the documentation at
  6. https://home-assistant.io/components/media_player.linkplay/
  7. """
  8.  
  9. import binascii
  10. import json
  11. import logging
  12. import os
  13. import tempfile
  14. import urllib.request as request2
  15. import xml.etree.ElementTree as ET
  16.  
  17. import homeassistant.helpers.config_validation as cv
  18. import requests
  19. import voluptuous as vol
  20. from homeassistant.components.media_player import (MediaPlayerDevice)
  21. from 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)
  26. from homeassistant.const import (
  27. ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, STATE_PAUSED, STATE_PLAYING,
  28. STATE_UNKNOWN)
  29. from homeassistant.util.dt import utcnow
  30.  
  31. from . import VERSION, ISSUE_URL, DATA_LINKPLAY
  32.  
  33. _LOGGER = logging.getLogger(__name__)
  34. ATTR_MASTER = 'master_id'
  35. ATTR_PRESET = 'preset'
  36. ATTR_SLAVES = 'slave_ids'
  37.  
  38. CONF_DEVICE_NAME = 'device_name'
  39. CONF_LASTFM_API_KEY = 'lastfm_api_key'
  40. #
  41. CONF_DEVICENAME_DEPRECATED = 'devicename' # TODO: Remove this deprecated key in version 3.0
  42.  
  43. DEFAULT_NAME = 'LinkPlay device'
  44.  
  45. LASTFM_API_BASE = "http://ws.audioscrobbler.com/2.0/?method="
  46.  
  47. LINKPLAY_CONNECT_MULTIROOM_SCHEMA = vol.Schema({
  48. vol.Required(ATTR_ENTITY_ID): cv.entity_id,
  49. vol.Required(ATTR_MASTER): cv.entity_id
  50. })
  51. LINKPLAY_PRESET_BUTTON_SCHEMA = vol.Schema({
  52. vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
  53. vol.Required(ATTR_PRESET): cv.positive_int
  54. })
  55. LINKPLAY_REMOVE_SLAVES_SCHEMA = vol.Schema({
  56. vol.Required(ATTR_ENTITY_ID): cv.entity_id,
  57. vol.Required(ATTR_SLAVES): cv.entity_ids
  58. })
  59.  
  60. MAX_VOL = 100
  61.  
  62.  
  63. def 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.  
  74. PLATFORM_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.  
  83. SERVICE_CONNECT_MULTIROOM = 'linkplay_connect_multiroom'
  84. SERVICE_PRESET_BUTTON = 'linkplay_preset_button'
  85. SERVICE_REMOVE_SLAVES = 'linkplay_remove_slaves'
  86.  
  87. SERVICE_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.  
  99. SUPPORT_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.  
  105. SOUND_MODES = {'0': 'Normal', '1': 'Classic', '2': 'Pop', '3': 'Jazz',
  106. '4': 'Vocal'}
  107. SOURCES = {'wifi': 'WiFi', 'line-in': 'Line-in', 'bluetooth': 'Bluetooth',
  108. 'optical': 'Optical', 'udisk': 'MicroSD'}
  109. SOURCES_MAP = {'0': 'WiFi', '10': 'WiFi', '31': 'WiFi', '40': 'Line-in',
  110. '41': 'Bluetooth', '43': 'Optical'}
  111. UPNP_TIMEOUT = 5
  112.  
  113.  
  114. # pylint: disable=W0613
  115. def 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
  161. class 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
  814. class 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
  847. class 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
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement