Skip to content

Commit

Permalink
Allow multiple Trakt instances with config flow (#38)
Browse files Browse the repository at this point in the history
* Create hassfest.yaml

* Delete hassfest.yaml

* Update Trakt integration to use config flow

* update when options change

* add hacs.json

* Allow mulitple instances, update readme
  • Loading branch information
engrbm87 authored May 3, 2020
1 parent 21ea547 commit 27cbf6e
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 105 deletions.
34 changes: 11 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# sensor.trakt

[Trakt](https://www.trakt.tv) component to feed [Upcoming Media Card](https://github.com/custom-cards/upcoming-media-card) with
Trakt's upcoming shows for [Home Assistant](https://www.home-assistant.io/)

Expand All @@ -13,24 +14,19 @@ Trakt's upcoming shows for [Home Assistant](https://www.home-assistant.io/)
[![Discord][discord-shield]][discord]
[![Community Forum][forum-shield]][forum]

## Installation:

1. Install this component by copying to your `/custom_components/trakt/` folder.
2. Add the code to your `configuration.yaml` using the config options below.
3. Restart
4. Add the Integration from the UI and setup your options
## Setup Trakt application on Trakt website

Note: The redirect_uri will be as follows:
* With HA cloud configured: https://<cloud-remote-url>/auth/external/callback
* Without HA cloud configured: http://<local-ip>:<port>/auth/external/callback or if base_url is used in HA -> https://<base-url>:<port>/auth/external/callback
1. Create new app at https://trakt.tv/oauth/applications
2. Use the following redirect_uri:
- With HA cloud configured: https://\<cloud-remote-url>/auth/external/callback
- Without HA cloud configured: http://\<local-ip>:<port>/auth/external/callback
3. Save the application and then note down the `client_id` and `client_secret`

**Example configuration.yaml:**
## Installation in Home Assistant:

```yaml
trakt:
client_id: <client_id>
client_secret: <client_secret>
```
1. Install this component by copying `custom_components/trakt` to your `config` folder (or install using `hacs`).
2. Restart Home Assistant
3. Add the Integration from the UI and setup your options

### Example ui-lovelace.yaml:

Expand All @@ -40,14 +36,6 @@ entity: sensor.upcoming_calendar
title: Upcoming Movies
```
**Configuration variables:**
key | type | description
:--- | :--- | :---
**client_id (Required)** | string | Client ID (create new app at https://trakt.tv/oauth/applications - use device auth in redirect and uri urn:ietf:wg:oauth:2.0:oob)
**client_secret (Required)** | string | Client Secret (create new app at https://trakt.tv/oauth/applications - use device auth in redirect)
***
Due to how `custom_components` are loaded, it is normal to see a `ModuleNotFoundError` error on first boot after adding this, to resolve it, restart Home-Assistant.

[buymecoffee]: https://www.buymeacoffee.com/iantrich
Expand Down
10 changes: 8 additions & 2 deletions custom_components/trakt/.translations/en.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
{
"config": {
"step": {
"pick_implementation": { "title": "Pick Authentication Method" }
"pick_implementation": { "title": "Pick Authentication Method" },
"user": {
"data": {
"client_id": "Client ID",
"client_secret": "Client secret"
}
}
},
"abort": {
"already_setup": "You can only configure one Trakt instance.",
"already_configured": "This client_id is already configured.",
"authorize_url_timeout": "Timeout generating authorize url.",
"missing_configuration": "The Trakt integration is not configured. Please follow the documentation."
},
Expand Down
102 changes: 34 additions & 68 deletions custom_components/trakt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import async_timeout
import homeassistant.util.dt as dt_util
import trakt
import voluptuous as vol
from homeassistant import config_entries, core
from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_CLIENT_ID,
Expand All @@ -23,7 +23,6 @@
CONF_SCAN_INTERVAL,
)
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from trakt.calendar import MyShowCalendar
Expand All @@ -33,7 +32,6 @@
from .const import (
CARD_DEFAULT,
CONF_DAYS,
DATA_TRAKT_CRED,
DATA_UPDATED,
DEFAULT_DAYS,
DEFAULT_SCAN_INTERVAL,
Expand All @@ -44,104 +42,90 @@

_LOGGER = logging.getLogger(__name__)

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)


async def async_setup(hass, config) -> bool:
"""Set up Trakt integration."""
hass.data[DOMAIN] = {}
"""Trakt integration doesn't support configuration.yaml."""

if DOMAIN not in config:
return True
return True


async def async_setup_entry(hass, entry) -> bool:
"""Set up Trakt from config entry."""

config_flow.TraktOAuth2FlowHandler.async_register_implementation(
hass,
config_entry_oauth2_flow.LocalOAuth2Implementation(
hass,
DOMAIN,
config[DOMAIN][CONF_CLIENT_ID],
config[DOMAIN][CONF_CLIENT_SECRET],
entry.data[CONF_CLIENT_ID],
entry.data[CONF_CLIENT_SECRET],
OAUTH2_AUTHORIZE,
OAUTH2_TOKEN,
),
)
hass.data.setdefault(
DATA_TRAKT_CRED,
{
CONF_CLIENT_ID: config[DOMAIN][CONF_CLIENT_ID],
CONF_CLIENT_SECRET: config[DOMAIN][CONF_CLIENT_SECRET],
},
)

return True


async def async_setup_entry(hass, entry) -> bool:
"""Set up Trakt from config entry."""

implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)

session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
await session.async_ensure_token_valid()

trakt_data = Trakt_Data(hass, entry, session)

if not await trakt_data.async_setup():
return False
hass.data[DOMAIN] = trakt_data

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = trakt_data
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "sensor")
)
return True


async def async_unload_entry(hass, entry):
async def async_unload_entry(hass, entry) -> bool:
"""Unload Trakt integration."""
if hass.data[DOMAIN].unsub_timer:
hass.data[DOMAIN].unsub_timer()
if hass.data[DOMAIN][entry.entry_id].unsub_timer:
hass.data[DOMAIN][entry.entry_id].unsub_timer()

await hass.config_entries.async_forward_entry_unload(entry, "sensor")

hass.data.pop(DOMAIN)
hass.data[DOMAIN].pop(entry.entry_id)

return True


class Trakt_Data:
"""Represent Trakt data."""

def __init__(self, hass, config_entry, implementation):
def __init__(
self,
hass: core.HomeAssistant,
config_entry: config_entries.ConfigEntry,
session: config_entry_oauth2_flow.OAuth2Session,
):
"""Initialize trakt data."""
self.hass = hass
self.config_entry = config_entry
self.session = config_entry_oauth2_flow.OAuth2Session(
hass, config_entry, implementation
)
self.session = session
self.unsub_timer = None
self.details = {}
self.calendar = None

@property
def days(self):
"""Return number of days to look forward for movies/shows."""
return self.config_entry.options[CONF_DAYS]
return self.config_entry.options.get(CONF_DAYS, DEFAULT_DAYS)

@property
def exclude(self):
"""Return list of show titles to exclude."""
return self.config_entry.options[CONF_EXCLUDE] or []
return self.config_entry.options.get(CONF_EXCLUDE) or []

@property
def scan_interval(self):
"""Return update interval."""
return self.config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)

async def async_update(self, *_):
"""Update Trakt data."""
Expand Down Expand Up @@ -220,16 +204,14 @@ async def async_update(self, *_):
card_json.append(card_item)

self.details = json.dumps(card_json)
_LOGGER.debug("Trakt data updated")

_LOGGER.debug("Trakt data updated")
async_dispatcher_send(self.hass, DATA_UPDATED)

async def async_setup(self):
"""Set up Trakt Data."""
await self.async_add_options()
trakt.core.OAUTH_TOKEN = self.session.token[CONF_ACCESS_TOKEN]
trakt.core.CLIENT_ID = self.hass.data[DATA_TRAKT_CRED][CONF_CLIENT_ID]
trakt.core.CLIENT_SECRET = self.hass.data[DATA_TRAKT_CRED][CONF_CLIENT_SECRET]
trakt.core.CLIENT_ID = self.config_entry.data[CONF_CLIENT_ID]

try:
await self.async_update()
Expand All @@ -239,27 +221,11 @@ async def async_setup(self):
)
return False

await self.async_set_scan_interval(
self.config_entry.options[CONF_SCAN_INTERVAL]
)
await self.async_set_scan_interval(self.scan_interval)
self.config_entry.add_update_listener(self.async_options_updated)

return True

async def async_add_options(self):
"""Add options for entry."""
if not self.config_entry.options:

options = {
CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
CONF_DAYS: DEFAULT_DAYS,
CONF_EXCLUDE: None,
}

self.hass.config_entries.async_update_entry(
self.config_entry, options=options
)

async def async_set_scan_interval(self, scan_interval):
"""Update scan interval."""

Expand All @@ -268,14 +234,14 @@ async def async_set_scan_interval(self, scan_interval):
self.unsub_timer = async_track_time_interval(
self.hass, self.async_update, timedelta(minutes=scan_interval)
)
await self.async_update()

@staticmethod
async def async_options_updated(hass, entry):
"""Triggered by config entry options updates."""
await hass.data[DOMAIN].async_set_scan_interval(
await hass.data[DOMAIN][entry.entry_id].async_set_scan_interval(
entry.options[CONF_SCAN_INTERVAL]
)
await hass.data[DOMAIN].async_update()


def days_until(date):
Expand Down
63 changes: 60 additions & 3 deletions custom_components/trakt/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,24 @@
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_EXCLUDE, CONF_SCAN_INTERVAL
from homeassistant.const import (
CONF_NAME,
CONF_EXCLUDE,
CONF_SCAN_INTERVAL,
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
)
from homeassistant.core import callback
from homeassistant.helpers import config_entry_oauth2_flow
from .const import CONF_DAYS, DEFAULT_DAYS, DEFAULT_SCAN_INTERVAL, DOMAIN
from .const import (
CONF_DAYS,
DEFAULT_DAYS,
DEFAULT_NAME,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
OAUTH2_AUTHORIZE,
OAUTH2_TOKEN,
)


class TraktOAuth2FlowHandler(
Expand All @@ -28,6 +42,49 @@ def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return TraktOptionsFlowHandler(config_entry)

def __init__(self):
"""Initialize the Trakt config flow."""
super().__init__()
self.config = None

async def async_step_user(self, user_input=None):
"""Handle a flow started by a user."""
if user_input:
await self.async_set_unique_id(user_input[CONF_CLIENT_ID])
self._abort_if_unique_id_configured()

self.config = user_input

TraktOAuth2FlowHandler.async_register_implementation(
self.hass,
config_entry_oauth2_flow.LocalOAuth2Implementation(
self.hass,
DOMAIN,
user_input[CONF_CLIENT_ID],
user_input[CONF_CLIENT_SECRET],
OAUTH2_AUTHORIZE,
OAUTH2_TOKEN,
),
)

return await self.async_step_pick_implementation()

return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
vol.Required(CONF_CLIENT_ID): str,
vol.Required(CONF_CLIENT_SECRET): str,
}
),
)

async def async_oauth_create_entry(self, data):
"""Create an entry for the flow."""
self.config.update(data)
return self.async_create_entry(title=self.config[CONF_NAME], data=self.config)


class TraktOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Trakt options."""
Expand All @@ -54,7 +111,7 @@ async def async_step_init(self, user_input=None):
default=self.config_entry.options.get(CONF_DAYS, DEFAULT_DAYS),
): int,
vol.Optional(
CONF_EXCLUDE, default=self.config_entry.options.get(CONF_EXCLUDE, None),
CONF_EXCLUDE, default=self.config_entry.options.get(CONF_EXCLUDE, "-"),
): str,
}

Expand Down
1 change: 0 additions & 1 deletion custom_components/trakt/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
CONF_EXCLUDE = "exclude"

DATA_UPDATED = "trakt_data_updated"
DATA_TRAKT_CRED = "trakt_credentials"

DEFAULT_DAYS = 30
DEFAULT_SCAN_INTERVAL = 60
Expand Down
Loading

0 comments on commit 27cbf6e

Please sign in to comment.