commit 2dae849d2ecb5efd449401d3e473d83b25bce1cc Author: Jeena Date: Fri Dec 20 11:14:49 2019 +0100 Initial commit diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4392f6d --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +HomeAssistant VLC HTTP component +================================ + +This component let's you add a remote VLC as a media_player to your +HomeAssistant instance. + +Preconditions +------------- + +- A computer which is accessible via network by HomeAssistant running VLC +- The Web interface in VLC is enabled, [see this how-to](https://www.howtogeek.com/117261/how-to-activate-vlcs-web-interface-control-vlc-from-a-browser-use-any-smartphone-as-a-remote/) +- This component is installed in HomeAssistant (*config*/custom\_components/vlc_http) +- This component is enabled in the configuration.yaml like this: + +```yaml +media_player: + - platform: vlc_http + host: 192.168.2.20 + port: 8080 + password: !secret vlc_http_password +``` + +After doing that you need to restart your HomeAssistant. + +Notes +----- + +It only works with HTTP because the VNC Web interface is only available via +HTTP and not HTTPS. + +You can autostart VNC on your computer and it will play for example text to +speach messages from HomeAssistant on it while you're on the computer, without +the need of an additional speaker connected to HomeAssistant. + +This component is basically a copy of [vlc_telnet](https://github.com/home-assistant/home-assistant/tree/dev/homeassistant/components/vlc_telnet) +but it replaces the Telnet backend with a HTTP backend. + +I hope I will be able to get it into HomeAssistant one day, perhaps by +extending the vlc_telnet component instead of having a copy of it. + +License +------- + +This code is under the same license as HomeAssistant: Apache License 2.0 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..3950bb8 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +"""The vlc remote http component.""" diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..4bdf553 --- /dev/null +++ b/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "vlc_http", + "name": "VLC http", + "documentation": "https://www.home-assistant.io/integrations/vlc-http", + "requirements": ["xmltodict==0.10.2"], + "dependencies": [], + "codeowners": ["@jeena"] +} diff --git a/media_player.py b/media_player.py new file mode 100644 index 0000000..be02cb4 --- /dev/null +++ b/media_player.py @@ -0,0 +1,269 @@ +"""Provide functionality to interact with the vlc http interface.""" +import logging + +from .vlc_http import ConnectionError as ConnErr, VLCHttp +import voluptuous as vol + +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, + SUPPORT_CLEAR_PLAYLIST, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_SHUFFLE_SET, + SUPPORT_STOP, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, +) +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + STATE_IDLE, + STATE_PAUSED, + STATE_PLAYING, + STATE_UNAVAILABLE, +) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "vlc_http" + +DEFAULT_NAME = "VLC-HTTP" +DEFAULT_PORT = 8080 + +SUPPORT_VLC = ( + SUPPORT_PAUSE + | SUPPORT_SEEK + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_MUTE + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_PLAY_MEDIA + | SUPPORT_STOP + | SUPPORT_CLEAR_PLAYLIST + | SUPPORT_PLAY + | SUPPORT_SHUFFLE_SET +) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the vlc platform.""" + add_entities( + [ + VlcDevice( + config.get(CONF_NAME), + config.get(CONF_HOST), + config.get(CONF_PORT), + config.get(CONF_PASSWORD), + ) + ], + True, + ) + + +class VlcDevice(MediaPlayerDevice): + """Representation of a vlc player.""" + + def __init__(self, name, host, port, passwd): + """Initialize the vlc device.""" + self._instance = None + self._name = name + self._volume = None + self._muted = None + self._state = STATE_UNAVAILABLE + self._media_position_updated_at = None + self._media_position = None + self._media_duration = None + self._host = host + self._port = port + self._password = passwd + self._vlc = None + self._available = False + self._volume_bkp = 0 + self._media_artist = "" + self._media_title = "" + + def update(self): + """Get the latest details from the device.""" + if self._vlc is None: + try: + self._vlc = VLCHttp(self._host, self._password, self._port) + self._state = STATE_IDLE + self._available = True + except (ConnErr, EOFError): + self._available = False + self._vlc = None + self._state = STATE_UNAVAILABLE + else: + try: + status = self._vlc.status() + if status: + if "volume" in status: + self._volume = int(status["volume"]) / 500.0 + else: + self._volume = None + if "state" in status: + state = status["state"] + if state == "playing": + self._state = STATE_PLAYING + elif state == "paused": + self._state = STATE_PAUSED + else: + self._state = STATE_IDLE + else: + self._state = STATE_UNAVAILABLE + + self._media_duration = self._vlc.get_length() + self._media_position = self._vlc.get_time() + + info = self._vlc.info() + if info: + self._media_artist = info[0].get("artist") + self._media_title = info[0].get("title") + + except (ConnErr, EOFError): + self._available = False + self._vlc = None + + return True + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_VLC + + @property + def media_content_type(self): + """Content type of current playing media.""" + return MEDIA_TYPE_MUSIC + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + return self._media_duration + + @property + def media_position(self): + """Position of current playing media in seconds.""" + return self._media_position + + @property + def media_position_updated_at(self): + """When was the position of the current playing media valid.""" + return self._media_position_updated_at + + @property + def media_title(self): + """Title of current playing media.""" + return self._media_title + + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + return self._media_artist + + def media_seek(self, position): + """Seek the media to a specific location.""" + track_length = self._vlc.get_length() / 1000 + self._vlc.seek(position / track_length) + + def mute_volume(self, mute): + """Mute the volume.""" + if mute: + self._volume_bkp = self._volume + self._volume = 0 + self._vlc.set_volume("0") + else: + self._vlc.set_volume(str(self._volume_bkp)) + self._volume = self._volume_bkp + + self._muted = mute + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._vlc.set_volume(str(volume * 500)) + self._volume = volume + + def media_play(self): + """Send play command.""" + self._vlc.play() + self._state = STATE_PLAYING + + def media_pause(self): + """Send pause command.""" + self._vlc.pause() + self._state = STATE_PAUSED + + def media_stop(self): + """Send stop command.""" + self._vlc.stop() + self._state = STATE_IDLE + + def play_media(self, media_type, media_id, **kwargs): + """Play media from a URL or file.""" + if media_type != MEDIA_TYPE_MUSIC: + _LOGGER.error( + "Invalid media type %s. Only %s is supported", + media_type, + MEDIA_TYPE_MUSIC, + ) + return + self._vlc.add(media_id) + self._state = STATE_PLAYING + + def media_previous_track(self): + """Send previous track command.""" + self._vlc.prev() + + def media_next_track(self): + """Send next track command.""" + self._vlc.next() + + def clear_playlist(self): + """Clear players playlist.""" + self._vlc.clear() + + def set_shuffle(self, shuffle): + """Enable/disable shuffle mode.""" + self._vlc.random(shuffle) + diff --git a/vlc_http.py b/vlc_http.py new file mode 100644 index 0000000..6f1bf98 --- /dev/null +++ b/vlc_http.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 + +""" +Library for contacting VLC through HTTP +""" + +import logging +import xmltodict +import requests +import json + +_LOGGER = logging.getLogger(__name__) + +class ConnectionError(Exception): + """Something is wrong with the connection to VLC.""" + pass + +class VLCHttp(object): + def __init__(self, host, password="", port=8080): + self._host = host + self._password = password + self._port = port + + def run_command(self, command="", attrs=""): + url = "http://{}:{}/requests/status.xml?command={}{}".format(self._host, self._port, command, attrs) + data=None + try: + req = requests.get(url, auth=("", self._password), timeout=1) + if req.status_code != 200: + raise ConnectionError("Query failed, response code: %s Full message: %s".format(req.status, req)) + + data = xmltodict.parse(req.text, process_namespaces=True).get("root") + + except Exception as error: + _LOGGER.debug("Failed communicating with VLC Server: %s".format(error)) + + try: + return data + except AttributeError: + _LOGGER.error("Received invalid response: %s".format(data)) + + def status(self): + return self.run_command() + + def get_length(self): + try: + return int(self.status()['length']) + except: + pass + + def get_time(self): + try: + return int(self.status()['time']) + except: + pass + + def info(self): + try: + category = self.status()['information']['category'] + if type(category) is list: + data = category[0]['info'] + if type(data) is list: + title = None + artist = None + for item in data: + if item['@name'] == 'title': + title = item['#text'] + elif item['@name'] == 'artist': + artist = item['#text'] + return [{"title": title, "artist": artist}] + elif data['@name'] == 'filename': + return [{"title": data['#text']}] + else: + return + except Exception: + return + + def seek(self, time): + self.run_command("seek&val={}".format(time)) + + def set_volume(self, new_volume): + self.run_command("volume&val={}".format(new_volume)) + + def play(self): + self.run_command("pl_play") + + def pause(self): + self.run_command("pl_pause") + + def stop(self): + self.run_command("pl_stop") + + def add(self, url): + self.run_command("in_play&input={}".format(url)) + + def prev(self): + self.run_command("pl_previous") + + def next(self): + self.run_command("pl_next") + + def clear(self): + self.run_command("pl_empty") + + def random(self): + self.run_command("pl_random") +