From 043f4a62779b1b427007e98f15b819e0ecab8160 Mon Sep 17 00:00:00 2001 From: marcelveldt Date: Tue, 14 May 2019 12:24:48 +0200 Subject: [PATCH] improve hass integration --- .gitignore | 1 + Dockerfile | 5 +- config.json | 6 ++- music_assistant/main.py | 16 +++--- music_assistant/modules/homeassistant.py | 51 +++++++++++-------- .../modules/playerproviders/lms.py | 3 +- music_assistant/{api.py => modules/web.py} | 49 ++++++++++++++---- music_assistant/player.py | 6 ++- music_assistant/web/pages/config.vue.js | 3 -- 9 files changed, 91 insertions(+), 49 deletions(-) rename music_assistant/{api.py => modules/web.py} (88%) diff --git a/.gitignore b/.gitignore index 452d444d..4e202d18 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ music_assistant/config.json *.cert *.pem +music_assistant/testrun.sh diff --git a/Dockerfile b/Dockerfile index a72525f2..cf37ea7d 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,15 @@ FROM python:3.7.3-alpine # install deps -RUN pip install --upgrade requirements.txt +RUN apk add build-base python-dev flac sox taglib-dev +COPY requirements.txt requirements.txt +RUN pip install --upgrade -r requirements.txt # copy files RUN mkdir -p /usr/src/app WORKDIR /usr/src/app COPY music_assistant /usr/src/app RUN chmod a+x /usr/src/app/main.py -RUN pip install --upgrade -r requirements.txt VOLUME ["/data"] diff --git a/config.json b/config.json index ef4eddad..4868e218 100755 --- a/config.json +++ b/config.json @@ -1,11 +1,13 @@ { "name": "Music Assistant", - "version": "0.0.1", + "version": "0.0.4", "description": "Media library manager for (streaming) media", "slug": "music_assistant", "startup": "application", "boot": "auto", - "map": [], + "arch": ["amd64"], + "map": ["share:rw","ssl"], + "webui": "http://[HOST]:[PORT:8095]", "host_network": true, "options": { }, diff --git a/music_assistant/main.py b/music_assistant/main.py index ad3835da..6f66ef49 100755 --- a/music_assistant/main.py +++ b/music_assistant/main.py @@ -15,16 +15,16 @@ import time from database import Database from metadata import MetaData -from api import Api from utils import run_periodic, LOGGER from cache import Cache from music import Music from player import Player from modules.homeassistant import setup as hass_setup +from modules.web import setup as web_setup class Main(): - def __init__(self, datapath, ssl_cert, ssl_key): + def __init__(self, datapath): uvloop.install() self._datapath = datapath self.parse_config() @@ -40,12 +40,12 @@ class Main(): self.db = Database(datapath, self.event_loop) # allow some time for the database to initialize while not self.db.db_ready: - time.sleep(0.5) + time.sleep(0.15) self.cache = Cache(datapath) self.metadata = MetaData(self.event_loop, self.db, self.cache) # init modules - self.api = Api(self, ssl_cert, ssl_key) + self.web = web_setup(self) self.hass = hass_setup(self) self.music = Music(self) self.player = Player(self) @@ -112,16 +112,14 @@ class Main(): ''' properly close all connections''' print('stop requested!') self.save_config() - self.api.stop() + self.web.stop() print('stopping event loop...') self.event_loop.stop() self.event_loop.close() if __name__ == "__main__": - datapath = sys.argv[1:] + datapath = sys.argv[1] if not datapath: datapath = os.path.dirname(os.path.abspath(__file__)) - ssl_cert = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'certificate.cert') - ssl_key = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'privkey.pem') - Main(datapath, ssl_cert, ssl_key) + Main(datapath) \ No newline at end of file diff --git a/music_assistant/modules/homeassistant.py b/music_assistant/modules/homeassistant.py index ad553937..699906b7 100644 --- a/music_assistant/modules/homeassistant.py +++ b/music_assistant/modules/homeassistant.py @@ -29,40 +29,42 @@ import slugify as slug def setup(mass): ''' setup the module and read/apply config''' - if not mass.config['base'].get('homeassistant'): - mass.config['base']['homeassistant'] = {} + create_config_entries(mass.config) conf = mass.config['base']['homeassistant'] - conf['__desc__'] = config_entries() - for key, def_value, desc in config_entries(): - if not key in conf: - conf[key] = def_value enabled = conf.get(CONF_ENABLED) token = conf.get('token') url = conf.get('url') if enabled and url and token: - # append hass player config settings - hass_player_conf = [("hass_power_entity", "", "Attach player power to homeassistant entity"), - ("hass_power_entity_source", "", "Source on the homeassistant entity (optional)"), - ("hass_volume_entity", "", "Attach player volume to homeassistant entity")] - for key, default, desc in hass_player_conf: - entry_found = False - for value in mass.config['player_settings']['__desc__']: - if value[0] == key: - entry_found = True - break - if not entry_found: - mass.config['player_settings']['__desc__'].append((key, default, desc)) return HomeAssistant(mass, url, token) return None -def config_entries(): +def create_config_entries(config): ''' get the config entries for this module (list with key/value pairs)''' - return [ + config_entries = [ (CONF_ENABLED, False, CONF_ENABLED), ('url', 'localhost', 'URL to homeassistant (e.g. https://homeassistant:8123)'), ('token', '', 'Long Lived Access Token'), ('publish_players', True, 'Publish players to Home Assistant') ] + if not config['base'].get('homeassistant'): + config['base']['homeassistant'] = {} + config['base']['homeassistant']['__desc__'] = config_entries + for key, def_value, desc in config_entries: + if not key in config['base']['homeassistant']: + config['base']['homeassistant'][key] = def_value + # append hass player config settings + if config['base']['homeassistant'][CONF_ENABLED]: + hass_player_conf = [("hass_power_entity", "", "Attach player power to homeassistant entity"), + ("hass_power_entity_source", "", "Source on the homeassistant entity (optional)"), + ("hass_volume_entity", "", "Attach player volume to homeassistant entity")] + for key, default, desc in hass_player_conf: + entry_found = False + for value in config['player_settings']['__desc__']: + if value[0] == key: + entry_found = True + break + if not entry_found: + config['player_settings']['__desc__'].append((key, default, desc)) class HomeAssistant(): ''' HomeAssistant integration ''' @@ -135,8 +137,15 @@ class HomeAssistant(): await self.mass.player.player_command(player_id, 'power', 'on') elif service == 'turn_off': await self.mass.player.player_command(player_id, 'power', 'off') + elif service == 'toggle': + await self.mass.player.player_command(player_id, 'power', 'toggle') elif service == 'volume_mute': - await self.mass.player.player_command(player_id, 'mute', service_data['is_volume_muted']) + args = 'on' if service_data['is_volume_muted'] else 'off' + await self.mass.player.player_command(player_id, 'mute', args) + elif service == 'volume_up': + await self.mass.player.player_command(player_id, 'volume', 'up') + elif service == 'volume_down': + await self.mass.player.player_command(player_id, 'volume', 'down') elif service == 'volume_set': volume_level = service_data['volume_level']*100 await self.mass.player.player_command(player_id, 'volume', volume_level) diff --git a/music_assistant/modules/playerproviders/lms.py b/music_assistant/modules/playerproviders/lms.py index ab086a72..85d26a21 100644 --- a/music_assistant/modules/playerproviders/lms.py +++ b/music_assistant/modules/playerproviders/lms.py @@ -187,7 +187,8 @@ class LMSProvider(PlayerProvider): # player is a groupplayer, retrieve childs group_player_child_ids = await self.__get_group_childs(player_id) for child_player_id in group_player_child_ids: - self._players[child_player_id].group_parent = player_id + if child_player_id in self._players: + self._players[child_player_id].group_parent = player_id elif player.group_parent: # check if player parent is still correct group_player_child_ids = await self.__get_group_childs(player.group_parent) diff --git a/music_assistant/api.py b/music_assistant/modules/web.py similarity index 88% rename from music_assistant/api.py rename to music_assistant/modules/web.py index 61552888..fbb9520e 100755 --- a/music_assistant/api.py +++ b/music_assistant/modules/web.py @@ -12,19 +12,49 @@ from functools import partial json_serializer = partial(json.dumps, default=lambda x: x.__dict__) import ssl -class Api(): - ''' expose our data through json api ''' +def setup(mass): + ''' setup the module and read/apply config''' + create_config_entries(mass.config) + conf = mass.config['base']['web'] + if conf['ssl_certificate'] and os.path.isfile(conf['ssl_certificate']): + ssl_cert = conf['ssl_certificate'] + else: + ssl_cert = '' + if conf['ssl_key'] and os.path.isfile(conf['ssl_key']): + ssl_key = conf['ssl_key'] + else: + ssl_key = '' + hostname = conf['hostname'] + return Web(mass, ssl_cert, ssl_key, hostname) + +def create_config_entries(config): + ''' get the config entries for this module (list with key/value pairs)''' + config_entries = [ + ('ssl_certificate', '', 'Path to ssl certificate file'), + ('ssl_key', '', 'Path to ssl keyfile'), + ('hostname', '', 'Hostname (FQDN used in the certificate)') + ] + if not config['base'].get('web'): + config['base']['web'] = {} + config['base']['web']['__desc__'] = config_entries + for key, def_value, desc in config_entries: + if not key in config['base']['web']: + config['base']['web'][key] = def_value + +class Web(): + ''' webserver and json/websocket api ''' - def __init__(self, mass, ssl_cert, ssl_key): + def __init__(self, mass, ssl_cert, ssl_key, hostname): self.mass = mass self._ssl_cert = ssl_cert self._ssl_key = ssl_key + self._hostname = hostname self.http_session = aiohttp.ClientSession() mass.event_loop.create_task(self.setup_web()) def stop(self): - self.runner.cleanup() - self.http_session.close() + asyncio.create_task(self.runner.cleanup()) + asyncio.create_task(self.http_session.close()) async def setup_web(self): app = web.Application() @@ -51,12 +81,13 @@ class Api(): self.runner = web.AppRunner(app) await self.runner.setup() - ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) - ssl_context.load_cert_chain(self._ssl_cert, self._ssl_key) http_site = web.TCPSite(self.runner, '0.0.0.0', 8095) - https_site = web.TCPSite(self.runner, '0.0.0.0', 8096, ssl_context=ssl_context) await http_site.start() - await https_site.start() + if self._ssl_cert and self._ssl_key: + ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_context.load_cert_chain(self._ssl_cert, self._ssl_key) + https_site = web.TCPSite(self.runner, '0.0.0.0', 8096, ssl_context=ssl_context) + await https_site.start() async def get_items(self, request): ''' get multiple library items''' diff --git a/music_assistant/player.py b/music_assistant/player.py index 444ba1b6..8a295c19 100755 --- a/music_assistant/player.py +++ b/music_assistant/player.py @@ -45,10 +45,12 @@ class Player(): # handle some common workarounds if cmd in ['pause', 'play'] and cmd_args == 'toggle': cmd = 'pause' if player.state == PlayerState.Playing else 'play' + if cmd == 'power' and cmd_args == 'toggle': + cmd_args = 'off' if player.powered else 'on' if cmd == 'volume' and cmd_args == 'up': - cmd_args = try_parse_int(cmd_args) + 2 + cmd_args = player.volume_level + 2 elif cmd == 'volume' and cmd_args == 'down': - cmd_args = try_parse_int(cmd_args) - 2 + cmd_args = player.volume_level - 2 if player.group_parent and cmd not in ['power', 'volume', 'mute']: # redirect playlist related commands to parent player return await self.player_command(player.group_parent, cmd, cmd_args) diff --git a/music_assistant/web/pages/config.vue.js b/music_assistant/web/pages/config.vue.js index ff2e8c36..55c2e166 100755 --- a/music_assistant/web/pages/config.vue.js +++ b/music_assistant/web/pages/config.vue.js @@ -22,9 +22,6 @@ var Config = Vue.component('Config', {