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]