From 984fdb7e574f72ac73afff42d723b2e92cfce050 Mon Sep 17 00:00:00 2001 From: Ryan Meek <25127328+maykar@users.noreply.github.com> Date: Fri, 19 Feb 2021 16:05:44 -0500 Subject: [PATCH] Dev (#98) * Cleanup and move code to helper functions * Update helpers.py * simplify run_start_script * scan for clients on call * Update README.md * remove responding check * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md --- README.md | 42 +++-- custom_components/plex_assistant/__init__.py | 155 ++++--------------- custom_components/plex_assistant/helpers.py | 77 ++++++++- 3 files changed, 129 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index 8174395..2850986 100644 --- a/README.md +++ b/README.md @@ -240,15 +240,39 @@ This option will trigger a script to start a Plex client if it is currently unav ``` "LivingRoom TV":"script.start_lr_plex", "Bedroom TV":"script.open_br_plex" ``` -The script would be different for every device and some devices might not have the ability to do this.
-An example of a script that would start the Plex app on a Roku device: +The script would be different for every device and some devices might not have the ability to do this.

+The example below would start the Plex app on a Roku device.
The script checks that both the app is open on the device and the app reports as available (take note of the comments in the code). + ``` -start_lr_plex: +roku_plex: sequence: - - condition: template - value_template: "{{ state_attr('media_player.roku', 'source') != 'Plex - Stream for Free' }}" - - service: media_player.select_source - entity_id: media_player.roku - data: - source: Plex - Stream for Free + - choose: + #### If Plex is already open on the device, do nothing + - conditions: + - condition: template + value_template: >- + {{ state_attr('media_player.roku','source') == 'Plex - Stream for Free' }} + sequence: [] + default: + #### If Plex isn't open on the device, open it + #### You could even add a service to turn your TV on here + - service: media_player.select_source + entity_id: 'media_player.roku' + data: + source: 'Plex - Stream for Free' + - repeat: + #### Wait until the Plex App/Client is available + while: + - condition: template + #### Loop until Plex App or client report as available and stop after 20 tries + value_template: >- + {{ (state_attr('media_player.roku','source') != 'Plex - Stream for Free' or + is_state('media_player.plex_plex_for_roku_roku', 'unavailable')) and + repeat.index <= 20 }} + sequence: + #### Scan to update device status + - service: plex.scan_for_clients + - delay: + seconds: 1 + mode: single ``` diff --git a/custom_components/plex_assistant/__init__.py b/custom_components/plex_assistant/__init__.py index 33aba6b..cf16e46 100644 --- a/custom_components/plex_assistant/__init__.py +++ b/custom_components/plex_assistant/__init__.py @@ -12,11 +12,8 @@ from homeassistant.core import Config, HomeAssistant from homeassistant.components.plex.services import get_plex_server from homeassistant.components.zeroconf import async_get_instance -from pychromecast.controllers.plex import PlexController -from datetime import timedelta import os -import json import time from .const import DOMAIN, _LOGGER @@ -24,19 +21,20 @@ from .process_speech import ProcessSpeech from .localize import translations from .helpers import ( - cast_next_prev, - device_responding, filter_media, find_media, fuzzy, get_devices, get_server, - jump, listeners, media_error, media_service, no_device_error, play_tts_error, + process_config_item, + remote_control, + run_start_script, + seek_to_offset, ) @@ -62,31 +60,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): tts_errors = entry.data.get("tts_errors") lang = entry.data.get("language") localize = translations[lang] - start_script = entry.options.get("start_script") - keyword_replace = entry.options.get("keyword_replace") + start_script = process_config_item(entry.options, "start_script") + keyword_replace = process_config_item(entry.options, "keyword_replace") jump_amount = [entry.options.get("jump_f") or 30, entry.options.get("jump_b") or 15] zeroconf = await async_get_instance(hass) - plex_c = PlexController() - - if start_script: - try: - start_script = json.loads("{" + start_script + "}") - if start_script: - for script in start_script.keys(): - _LOGGER.debug(f"Script {script}: {start_script[script]}") - except: - start_script = None - _LOGGER.warning("Plex Assistant: There is a formatting issue with your client start script config.") - start_script_keys = list(start_script.keys()) if start_script else [] - - if keyword_replace: - try: - keyword_replace = json.loads("{" + keyword_replace + "}") - for word in keyword_replace.keys(): - _LOGGER.debug(f"Replace '{word}' with '{keyword_replace[word]}'") - except: - keyword_replace = None - _LOGGER.warning("Plex Assistant: There is a formatting issue with your keyword replacement config.") server = await get_server(hass, hass.config, server_name) if not server: @@ -97,7 +74,7 @@ def pa_executor(_server, start_script_keys): get_devices(hass, _pa) return _pa - pa = await hass.async_add_executor_job(pa_executor, server, start_script_keys) + pa = await hass.async_add_executor_job(pa_executor, server, list(start_script.keys())) ifttt_listener = await listeners(hass) hass.data[DOMAIN][entry.entry_id] = {"remove_listener": ifttt_listener} @@ -109,30 +86,23 @@ def pa_executor(_server, start_script_keys): entry.add_update_listener(async_reload_entry) def handle_input(call): - offset = None - media = None - + hass.services.async_call("plex", "scan_for_clients", blocking=False, limit=30) command = call.data.get("command").strip() - _LOGGER.debug("Command: %s", command) + media = None if not command: _LOGGER.warning(localize["no_call"]) return + _LOGGER.debug("Command: %s", command) command = command.lower() - if keyword_replace and any(keyword.lower() in command for keyword in keyword_replace.keys()): for keyword in keyword_replace.keys(): command = command.replace(keyword.lower(), keyword_replace[keyword].lower()) get_devices(hass, pa) - - command = ProcessSpeech(pa, localize, command, default_device) - command = command.results - - command_debug = {i: command[i] for i in command if i != "library" and command[i]} - command_debug = str(command_debug).replace("'", "").replace(":", " =") - _LOGGER.debug(f"Processed Command: {command_debug[1:-1]}") + command = ProcessSpeech(pa, localize, command, default_device).results + _LOGGER.debug("Processed Command: %s", {i: command[i] for i in command if i != "library" and command[i]}) if not command["device"] and not default_device: no_device_error(localize) @@ -142,118 +112,47 @@ def handle_input(call): pa.update_libraries() device = fuzzy(command["device"] or default_device, pa.device_names) - - responding = True - if device[0] in start_script_keys: - timeout = 0 - started = False - responding = False - woken = False - start_time = time.time() - while timeout < 30 and device[0] not in pa.devices: - started = True - if timeout == 0: - hass.services.call("script", start_script[device[0]].replace("script.", "")) - time.sleep(5) - hass.services.call("plex", "scan_for_clients") - else: - time.sleep(1) - if (timeout % 2) == 0 or timeout == 0: - get_devices(hass, pa) - timeout += 1 - - if started: - hass.services.async_call("plex", "scan_for_clients") - get_devices(hass, pa) - - if device[0] in pa.devices: - stop = False - while not responding and not stop: - if not started: - hass.services.call("script", start_script[device[0]].replace("script.", "")) - responding = device_responding(hass, pa, device[0]) - stop = True - total_time = timedelta(seconds=time.time()) - timedelta(seconds=start_time) - - if responding and not started and total_time > timedelta(seconds=1): - time.sleep(5) - - device = fuzzy(command["device"] or default_device, list(pa.devices.keys())) + device = run_start_script(hass, pa, command, start_script, device) _LOGGER.debug("PA Devices: %s", pa.devices) - - if device[1] < 60 or not responding: + if device[1] < 60: no_device_error(localize, command["device"]) return - else: - _LOGGER.debug("Device: %s", device[0]) - device = pa.devices[device[0]] + _LOGGER.debug("Device: %s", device[0]) - if command["control"] == "jump_forward": - jump(hass, device, jump_amount[0]) - return - elif command["control"] == "jump_back": - jump(hass, device, -jump_amount[1]) - return - elif command["control"] == "next_track" and device["device_type"] == "cast": - cast_next_prev(hass, zeroconf, plex_c, device, "next") - return - elif command["control"] == "previous_track" and device["device_type"] == "cast": - cast_next_prev(hass, zeroconf, plex_c, device, "previous") - return - elif command["control"]: - media_service(hass, device["entity_id"], f"media_{command['control']}") + device = pa.devices[device[0]] + + if command["control"]: + remote_control(hass, zeroconf, command["control"], device) return try: - result = find_media(command, command["media"], pa.media) - media = filter_media(pa, command, result["media"], result["library"]) + media, library = find_media(command, command["media"], pa.media) + media = filter_media(pa, command, media, library) except: error = media_error(command, localize) if tts_errors: play_tts_error(hass, tts_dir, device["entity_id"], error, lang) - _LOGGER.debug("Media: %s", str(media)) - if getattr(media, "viewOffset", 0) > 10 and not command["random"]: - offset = (media.viewOffset / 1000) - 5 - shuffle = 1 if command["random"] else 0 + offset = (media.viewOffset / 1000) - 5 if getattr(media, "viewOffset", 0) > 15 and not command["random"] else 0 if getattr(media, "TYPE", None) == "episode": episodes = media.show().episodes() episodes = episodes[episodes.index(media):] media = pa.server.createPlayQueue(episodes, shuffle=shuffle) - - if not getattr(media, "TYPE", None) == "playqueue": + elif not getattr(media, "TYPE", None) == "playqueue": media = pa.server.createPlayQueue(media, shuffle=shuffle) - payload = '{"playqueue_id": %s, "type": "%s"}' % ( + payload = '%s{"playqueue_id": %s, "type": "%s"}' % ( + "plex://" if device["device_type"] in ["cast", "sonos"] else "", media.playQueueID, - media.playQueueType, + media.playQueueType ) - if device["device_type"] == "cast": - payload = "plex://" + payload - media_service(hass, device["entity_id"], "play_media", payload) - - if offset: - timeout = 0 - while not hass.states.is_state(device["entity_id"], "playing") and timeout < 200: - time.sleep(0.25) - timeout += 1 - - if device["device_type"] == "cast": - timeout = 0 - while hass.states.get(device["entity_id"]).attributes.get("media_position", 0) == 0 and timeout < 200: - time.sleep(0.25) - timeout += 1 - else: - time.sleep(0.75) - - if hass.states.is_state(device["entity_id"], "playing"): - media_service(hass, device["entity_id"], "media_seek", offset) + seek_to_offset(hass, offset, device["entity_id"]) hass.services.async_register(DOMAIN, "command", handle_input) return True diff --git a/custom_components/plex_assistant/helpers.py b/custom_components/plex_assistant/helpers.py index 9ac00b4..aa4e40c 100644 --- a/custom_components/plex_assistant/helpers.py +++ b/custom_components/plex_assistant/helpers.py @@ -1,16 +1,40 @@ import re import time import uuid +import json import pychromecast from fuzzywuzzy import fuzz from fuzzywuzzy import process as fw +from datetime import timedelta from gtts import gTTS from homeassistant.components.plex.services import get_plex_server +from homeassistant.core import Context +from pychromecast.controllers.plex import PlexController from .const import DOMAIN, _LOGGER +def fuzzy(media, lib, scorer=fuzz.QRatio): + if isinstance(lib, list) and len(lib) > 0: + return fw.extractOne(media, lib, scorer=scorer) + return ["", 0] + + +def process_config_item(options, item_type): + item = options.get(item_type) + if item: + try: + item = json.loads("{" + item + "}") + for i in item.keys(): + _LOGGER.debug(f"{item_type} {i}: {item[i]}") + except: + item = {} + return item + else: + return {} + + async def get_server(hass, config, server_name): try: await hass.helpers.discovery.async_discover(None, None, "plex", config) @@ -45,12 +69,20 @@ def device_responding(hass, pa, device): "media_player", "media_play", {"entity_id": pa.devices[device]["entity_id"]}, - blocking = True, + blocking=True, limit=30 ) return responding +def run_start_script(hass, pa, command, start_script, device): + if device[0] in start_script.keys(): + start = hass.data["script"].get_entity(start_script[device[0]]) + start.script.run(context=Context()) + get_devices(hass, pa) + return fuzzy(command["device"] or default_device, list(pa.devices.keys())) + return device + async def listeners(hass): def ifttt_webhook_callback(event): @@ -101,6 +133,41 @@ def cast_next_prev(hass, zeroconf, plex_c, device, direction): plex_c.previous() +def remote_control(hass, zeroconf, control, device): + plex_c = PlexController() + + if control == "jump_forward": + jump(hass, device, jump_amount[0]) + elif control == "jump_back": + jump(hass, device, -jump_amount[1]) + elif control == "next_track" and device["device_type"] == "cast": + cast_next_prev(hass, zeroconf, plex_c, device, "next") + elif control == "previous_track" and device["device_type"] == "cast": + cast_next_prev(hass, zeroconf, plex_c, device, "previous") + else: + media_service(hass, device["entity_id"], f"media_{control}") + + +def seek_to_offset(hass, offset, entity): + if offset > 0: + timeout = 0 + while not hass.states.is_state(entity, "playing") and timeout < 100: + time.sleep(0.10) + timeout += 1 + + timeout = 0 + if hass.states.is_state(entity, "playing"): + media_service(hass, entity, "media_pause") + while not hass.states.is_state(entity, "paused") and timeout < 100: + time.sleep(0.10) + timeout += 1 + + if hass.states.is_state(entity, "paused"): + if hass.states.get(entity).attributes.get("media_position", 0) < 9: + media_service(hass, entity, "media_seek", offset) + media_service(hass, entity, "media_play") + + def no_device_error(localize, device=None): device = f': "{device.title()}".' if device else "." _LOGGER.warning( @@ -140,12 +207,6 @@ def play_tts_error(hass, tts_dir, device, error, lang): ) -def fuzzy(media, lib, scorer=fuzz.QRatio): - if isinstance(lib, list) and len(lib) > 0: - return fw.extractOne(media, lib, scorer=scorer) - return ["", 0] - - def get_title(item, deep=False): if item.type == "movie": return item.title @@ -257,4 +318,4 @@ def find_media(selected, media, lib): result = movie_test[0] library = lib["movies"] - return {"media": result, "library": library} + return [result, library]