Skip to content

Commit 835f940

Browse files
committed
Update
- rewriten to work with config flow - added support for multiple instances - added name parameter which is used for instance name and id (the unique id is generated from apikey) - removed necessity to download images (also because of that removed "img_dir", "image_resolution", "download_images") - removed "ssl_cert" because it was just bypass for ssl - rewrited data parsing to work with xml instead of json
1 parent e16e4d1 commit 835f940

File tree

14 files changed

+637
-429
lines changed

14 files changed

+637
-429
lines changed

README.md

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,31 @@ Read through these two resources before posting issues to GitHub or the forums.
1717
## Installation:
1818
1. Install this component by copying [these files](https://github.com/custom-components/sensor.plex_recently_added/tree/master/custom_components/plex_recently_added) to `custom_components/plex_recently_added/`.
1919
2. Install the card: [Upcoming Media Card](https://github.com/custom-cards/upcoming-media-card)
20-
3. Add the code to your `configuration.yaml` using the config options below.
21-
4. Add the code for the card to your `ui-lovelace.yaml`.
22-
5. **You will need to restart after installation for the component to start working.**
23-
24-
### Options
25-
26-
| key | default | required | description |
27-
| ---------------- | ----------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
28-
| name | Plex_Recently_Added | no | Name of the sensor. Useful to make multiple sensors with different libraries. |
29-
| token | | yes | Your Plex token [(Find your Plex token)](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/) |
30-
| host | localhost | yes | The host Plex is running on. |
31-
| port | 32400 | yes | The port Plex is running on. |
32-
| ssl | false | no | Set to true if you use SSL to access Plex. |
33-
| max | 5 | no | Max number of items to show in sensor. |
34-
| on_deck | False | no | Set to true to show "on deck" items. |
35-
| download_images | true | no | Setting this to false will turn off downloading of images, but will require certain Plex settings to work. See below. |
36-
| img_dir | '/upcoming-media-card-images/plex/' | no | This option allows you to choose a custom directory to store images in if you enable download_images. Directory must start and end with a `/`. |
37-
| ssl_cert | false | no | If you provide your own SSL certificate in Plex's network settings set this to true. |
38-
| section_types | movie, show | no | Allows you to specify which section types to consider [movie, show]. |
39-
| image_resolution | 200 | no | Allows you to change the resolution of the generated images (in px), useful to display higher quality images as a background somewhere. |
40-
| exclude_keywords | | no | Allows you to specify a list of keywords to be exclude from the sensor if in the title. |
41-
42-
#### By default this addon automatically downloads images from Plex to your /www/custom-lovelace/upcoming-media-card/ directory. The directory is automatically created & only images reported in the upcoming list are downloaded. Images are small in size and are removed automatically when no longer needed. Currently & unfortunately, this may not work on all systems.
43-
44-
#### If you prefer to not download the images you may set download_images to false, but you either have to set "Secure connections" to "preferred" or "disabled" (no SSL) or have a custom certificate set (these options are found in your Plex server's network settings). This is needed because the default SSL certificate supplied by Plex is for their own domain and not for your Plex server. Your server also needs to be "fully accessible outside your network" if you wish to be able to see images remotely. If your Plex server provides it's own certificate you only need to set ssl_cert to true and download_images to false.
45-
46-
</br></br>
20+
3. Add the code for the card to your `ui-lovelace.yaml`.
21+
4. **You will need to restart after installation for the component to start working.**
22+
23+
### Adding device
24+
To add the **Plex Recently added** integration to your Home Assistant, use this My button:
25+
26+
<a href="https://my.home-assistant.io/redirect/config_flow_start?domain=plex_recently_added" class="my badge" target="_blank"><img src="https://my.home-assistant.io/badges/config_flow_start.svg"></a>
27+
28+
<details><summary style="list-style: none"><h3><b style="cursor: pointer">Manual configuration steps</b></h3></summary>
29+
30+
If the above My button doesn’t work, you can also perform the following steps manually:
31+
32+
- Browse to your Home Assistant instance.
33+
34+
- Go to [Settings > Devices & Services](https://my.home-assistant.io/redirect/integrations/).
35+
36+
- In the bottom right corner, select the [Add Integration button.](https://my.home-assistant.io/redirect/config_flow_start?domain=plex_recently_added)
37+
38+
- From the list, select **Plex Recently added**.
39+
40+
- Follow the instructions on screen to complete the setup.
41+
</details>
42+
43+
The number of items in sensor, library types, libraries in general, excluded words and show "on deck" options can be changed later.
44+
4745
**Do not just copy examples, please use config options above to build your own!**
4846
### Sample for minimal config needed in configuration.yaml:
4947
```yaml
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,75 @@
1+
from homeassistant.config_entries import ConfigEntry
2+
from homeassistant.const import Platform
3+
from homeassistant.core import HomeAssistant
4+
from homeassistant.exceptions import ConfigEntryNotReady
5+
from homeassistant.const import (
6+
CONF_API_KEY,
7+
CONF_HOST,
8+
CONF_PORT,
9+
CONF_SSL
10+
)
111

12+
from .const import (
13+
DOMAIN,
14+
CONF_TOKEN,
15+
CONF_MAX,
16+
CONF_SECTION_TYPES,
17+
CONF_SECTION_LIBRARIES,
18+
CONF_EXCLUDE_KEYWORDS,
19+
CONF_ON_DECK,
20+
CONF_LOCAL
21+
)
22+
23+
24+
from .coordinator import PlexDataCoordinator
25+
from .helpers import setup_client
26+
from .plex_api import (
27+
FailedToLogin,
28+
)
29+
30+
31+
PLATFORMS = [
32+
Platform.SENSOR
33+
]
34+
35+
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
36+
try:
37+
client = await hass.async_add_executor_job(
38+
setup_client,
39+
hass,
40+
config_entry.data[CONF_SSL],
41+
config_entry.data[CONF_API_KEY],
42+
config_entry.data[CONF_MAX],
43+
config_entry.data[CONF_ON_DECK],
44+
config_entry.data[CONF_HOST],
45+
config_entry.data[CONF_PORT],
46+
config_entry.data.get(CONF_SECTION_TYPES, []),
47+
config_entry.data.get(CONF_SECTION_LIBRARIES, []),
48+
config_entry.data.get(CONF_EXCLUDE_KEYWORDS, []),
49+
config_entry.data[CONF_LOCAL],
50+
)
51+
except FailedToLogin as err:
52+
raise ConfigEntryNotReady("Failed to Log-in") from err
53+
coordinator = PlexDataCoordinator(hass, client)
54+
55+
await coordinator.async_config_entry_first_refresh()
56+
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
57+
58+
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
59+
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
60+
61+
return True
62+
63+
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
64+
"""Unload Plex config entry."""
65+
if unload_ok := await hass.config_entries.async_unload_platforms(
66+
config_entry, PLATFORMS
67+
):
68+
del hass.data[DOMAIN][config_entry.entry_id]
69+
if not hass.data[DOMAIN]:
70+
del hass.data[DOMAIN]
71+
return unload_ok
72+
73+
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
74+
"""Handle options update."""
75+
await hass.config_entries.async_reload(config_entry.entry_id)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from typing import Any
2+
3+
import voluptuous as vol
4+
5+
from homeassistant.helpers.selector import (
6+
SelectSelector,
7+
SelectSelectorConfig,
8+
SelectSelectorMode,
9+
TextSelector,
10+
TextSelectorConfig
11+
)
12+
from homeassistant.config_entries import ConfigEntry
13+
from homeassistant.core import callback
14+
from homeassistant.config_entries import ConfigFlow
15+
from homeassistant.const import (
16+
CONF_API_KEY,
17+
CONF_NAME,
18+
CONF_HOST,
19+
CONF_PORT,
20+
CONF_SSL
21+
)
22+
23+
from .const import (
24+
DOMAIN,
25+
DEFAULT_NAME,
26+
CONF_MAX,
27+
CONF_SECTION_TYPES,
28+
ALL_SECTION_TYPES,
29+
CONF_SECTION_LIBRARIES,
30+
CONF_EXCLUDE_KEYWORDS,
31+
CONF_ON_DECK,
32+
CONF_LOCAL
33+
)
34+
35+
from .helpers import setup_client
36+
from .plex_api import (
37+
FailedToLogin,
38+
)
39+
from .options_flow import PlexOptionFlow
40+
41+
PLEX_SCHEMA = vol.Schema({
42+
vol.Optional(CONF_NAME, default=''): vol.All(str),
43+
vol.Required(CONF_HOST, default='localhost'): vol.All(str),
44+
vol.Required(CONF_PORT, default=32400): vol.All(vol.Coerce(int), vol.Range(min=0)),
45+
vol.Required(CONF_API_KEY): vol.All(str),
46+
vol.Optional(CONF_SSL, default=False): vol.All(bool),
47+
vol.Optional(CONF_LOCAL, default=False): vol.All(bool),
48+
vol.Optional(CONF_MAX, default=5): vol.All(vol.Coerce(int), vol.Range(min=0)),
49+
vol.Optional(CONF_ON_DECK, default=False): vol.All(bool),
50+
vol.Optional(CONF_SECTION_TYPES, default={"movie", "show"}): SelectSelector(SelectSelectorConfig(options=ALL_SECTION_TYPES, mode=SelectSelectorMode.DROPDOWN ,multiple=True)),
51+
vol.Optional(CONF_SECTION_LIBRARIES): TextSelector(TextSelectorConfig(multiple=True, multiline=False)),
52+
vol.Optional(CONF_EXCLUDE_KEYWORDS): TextSelector(TextSelectorConfig(multiple=True, multiline=False)),
53+
})
54+
55+
class PlexConfigFlow(ConfigFlow, domain=DOMAIN):
56+
"""Config flow for the Plex integration."""
57+
@staticmethod
58+
@callback
59+
def async_get_options_flow(config_entry: ConfigEntry) -> PlexOptionFlow:
60+
return PlexOptionFlow(config_entry)
61+
62+
async def async_step_user(
63+
self, user_input: dict[str, Any] | None = None
64+
):
65+
errors = {}
66+
67+
if user_input is not None:
68+
self._async_abort_entries_match({CONF_API_KEY: user_input[CONF_API_KEY]})
69+
try:
70+
await self.hass.async_add_executor_job(
71+
setup_client,
72+
self.hass,
73+
user_input[CONF_SSL],
74+
user_input[CONF_API_KEY],
75+
user_input[CONF_MAX],
76+
user_input[CONF_ON_DECK],
77+
user_input[CONF_HOST],
78+
user_input[CONF_PORT],
79+
user_input.get(CONF_SECTION_TYPES, []),
80+
user_input.get(CONF_SECTION_LIBRARIES, []),
81+
user_input.get(CONF_EXCLUDE_KEYWORDS, []),
82+
user_input[CONF_LOCAL],
83+
)
84+
except FailedToLogin as err:
85+
errors = {'base': 'failed_to_login'}
86+
else:
87+
return self.async_create_entry(title=user_input[CONF_NAME] if len(user_input[CONF_NAME]) > 0 else DEFAULT_NAME, data=user_input)
88+
89+
schema = self.add_suggested_values_to_schema(PLEX_SCHEMA, user_input)
90+
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from typing import Final
2+
3+
DOMAIN: Final = "plex_recently_added"
4+
5+
6+
DEFAULT_NAME: Final = 'Plex Recently Added'
7+
CONF_TOKEN: Final = 'token'
8+
CONF_MAX: Final = 'max'
9+
CONF_SECTION_TYPES: Final = 'section_types'
10+
ALL_SECTION_TYPES: Final = ["movie", "show", "artist", "photo"]
11+
CONF_SECTION_LIBRARIES: Final = 'section_libraries'
12+
CONF_EXCLUDE_KEYWORDS: Final = 'exclude_keywords'
13+
CONF_ON_DECK: Final = 'on_deck'
14+
CONF_LOCAL: Final = 'is_local'
15+
16+
17+
DEFAULT_PARSE_DICT: Final = {
18+
'title_default': '$title',
19+
'line1_default': '$episode',
20+
'line2_default': '$release',
21+
'line3_default': '$number - $rating - $runtime',
22+
'line4_default': '$genres',
23+
'icon': 'mdi:eye-off'
24+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from datetime import timedelta
2+
import logging
3+
from typing import Dict, Any
4+
5+
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
6+
from homeassistant.core import HomeAssistant
7+
from homeassistant.exceptions import ConfigEntryError
8+
9+
from .const import DOMAIN
10+
from .plex_api import (
11+
PlexApi,
12+
FailedToLogin,
13+
)
14+
15+
_LOGGER = logging.getLogger(__name__)
16+
17+
class PlexDataCoordinator(DataUpdateCoordinator[Dict[str, Any]]):
18+
def __init__(self, hass: HomeAssistant, client: PlexApi):
19+
self._client = client
20+
21+
super().__init__(
22+
hass,
23+
_LOGGER,
24+
name=DOMAIN,
25+
update_method=self._async_update_data,
26+
update_interval=timedelta(minutes=10),
27+
)
28+
29+
async def _async_update_data(self) -> Dict[str, Any]:
30+
try:
31+
return await self.hass.async_add_executor_job(self._client.update)
32+
except FailedToLogin:
33+
raise ConfigEntryError("Failed to Log-in") from err
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from homeassistant.core import HomeAssistant
2+
from .plex_api import PlexApi
3+
4+
def setup_client(
5+
hass: HomeAssistant,
6+
ssl: bool,
7+
token: str,
8+
max: int,
9+
on_deck: bool,
10+
host: str,
11+
port: int,
12+
section_types: list,
13+
section_libraries: list,
14+
exclude_keywords: list,
15+
is_local: bool,
16+
):
17+
client = PlexApi(hass, ssl, token, max, on_deck, host, port, section_types, section_libraries, exclude_keywords, is_local)
18+
19+
client.update()
20+
return client

custom_components/plex_recently_added/manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"domain": "plex_recently_added",
33
"name": "Plex Recently Added",
44
"codeowners": ["@maykar"],
5+
"config_flow": true,
56
"dependencies": [],
67
"documentation": "https://github.com/custom-components/sensor.plex_recently_added",
78
"iot_class": "local_polling",
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from typing import Any
2+
from homeassistant.config_entries import OptionsFlow, ConfigEntry, ConfigFlowResult
3+
import voluptuous as vol
4+
5+
from homeassistant.helpers.selector import (
6+
SelectSelector,
7+
SelectSelectorConfig,
8+
SelectSelectorMode,
9+
TextSelector,
10+
TextSelectorConfig
11+
)
12+
13+
from .const import (
14+
DOMAIN,
15+
CONF_SECTION_TYPES,
16+
CONF_SECTION_LIBRARIES,
17+
ALL_SECTION_TYPES,
18+
CONF_EXCLUDE_KEYWORDS,
19+
CONF_MAX,
20+
CONF_ON_DECK,
21+
)
22+
23+
24+
class PlexOptionFlow(OptionsFlow):
25+
def __init__(self, config_entry: ConfigEntry) -> None:
26+
self._config_entry = config_entry
27+
28+
29+
async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
30+
errors = {}
31+
32+
coordinator = self.hass.data[DOMAIN][self._config_entry.entry_id]
33+
34+
if user_input is not None:
35+
# Validate and process user input here
36+
updated_data = {
37+
**self._config_entry.data,
38+
CONF_MAX: user_input.get(CONF_MAX, 5),
39+
CONF_SECTION_TYPES: user_input.get(CONF_SECTION_TYPES, []),
40+
CONF_SECTION_LIBRARIES: user_input.get(CONF_SECTION_LIBRARIES, []),
41+
CONF_EXCLUDE_KEYWORDS: user_input.get(CONF_EXCLUDE_KEYWORDS, []),
42+
CONF_ON_DECK: user_input.get(CONF_ON_DECK, False),
43+
}
44+
self._config_entry.data = updated_data
45+
46+
return self.async_create_entry(title="", data=updated_data)
47+
48+
PLEX_SCHEMA = vol.Schema({
49+
vol.Optional(CONF_MAX, default=self._config_entry.data[CONF_MAX]): vol.All(vol.Coerce(int), vol.Range(min=0)),
50+
vol.Optional(CONF_SECTION_TYPES, default=self._config_entry.data.get(CONF_SECTION_TYPES, [])): SelectSelector(SelectSelectorConfig(options=ALL_SECTION_TYPES ,multiple=True, mode=SelectSelectorMode.DROPDOWN)),
51+
vol.Optional(CONF_SECTION_LIBRARIES, default=self._config_entry.data.get(CONF_SECTION_LIBRARIES, [])): SelectSelector(SelectSelectorConfig(options=[str(item) for item in coordinator.data["libraries"]] ,multiple=True, mode=SelectSelectorMode.DROPDOWN)),
52+
vol.Optional(CONF_EXCLUDE_KEYWORDS, default=self._config_entry.data.get(CONF_EXCLUDE_KEYWORDS, [])): TextSelector(TextSelectorConfig(multiple=True, multiline=False)),
53+
vol.Optional(CONF_ON_DECK, default=self._config_entry.data.get(CONF_ON_DECK, False)): vol.All(bool),
54+
})
55+
56+
# Display a form to gather user input
57+
return self.async_show_form(step_id="init", data_schema=PLEX_SCHEMA, errors=errors)
58+
59+
def keys(d) -> list:
60+
return [i for i in d.keys()]

0 commit comments

Comments
 (0)