Skip to content

Commit

Permalink
Refactor to work with a config_flow (#67)
Browse files Browse the repository at this point in the history
* feat: add config_flow to replace manual configuration
* feat: no need for local python script or docker container
* feat: add coordinator for polling data
* feat: add DE translation
* refactor: ... hm ... everything?
  • Loading branch information
mricharz authored Feb 24, 2023
1 parent 6b86b91 commit fca49e9
Show file tree
Hide file tree
Showing 12 changed files with 481 additions and 305 deletions.
116 changes: 25 additions & 91 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,100 +3,34 @@
This aim to show the stock of one or multiple [TooGoodToGo](https://toogoodtogo.com/) item using the [tgtg-python](https://github.com/ahivert/tgtg-python) library.
Sensor data can be used afterward to generate notifications, history graphs, ... share your best examples in the [Discussion tab](https://github.com/Chouffy/home_assistant_tgtg/discussions)!

## Features

- Fetch each item stock defined
- Authenticate using tokens
- Retrieve all favorites instead of a manual list of item_id if no `item:` are defined
- Retrieve additional information as attributes, if available:
- Item ID
- TooGoodToGo price and original value
- Pick-up start and end
- Sold-out date

## Installation

Two steps are required:

1. Get access tokens, either manually or via Docker
1. Install the integration

### 1. Get access tokens

First you'll need to get access tokens for this integration to work.

This is to be executed outside of Home Assistant, i.e. on your local machine.

#### 1a. Manually

1. Install required packages.
- [Python >=3.8](https://www.python.org/downloads/)
- [tgtg-python](https://github.com/ahivert/tgtg-python) library: In a command line, type `pip install tgtg>=0.11.0` or `pip install --upgrade tgtg` if you already have it.
1. Run the [tgtg_get_tokens](./tgtg_get_tokens.py) script to get access and refresh token. Save these for later.

#### 1b. Docker

_This is work in progress._

You only need Docker installed. There is no need to clone the repo because Docker can build from an external URL.
## Usage

```
docker build https://github.com/Chouffy/home_assistant_tgtg.git#main --tag "homeassistant_tgtg_tokens:latest"
docker run --rm -it homeassistant_tgtg_tokens
```
### Installation via [HACS](https://hacs.xyz/)

### 2. Installation via [HACS](https://hacs.xyz/)

1. Search for _TooGoodToGo_ in the Integration tab of HACS
1. Click _Install_
1. Copy over the tokens in `/config/configuration.yaml` retrieved in 1.
1. Search for *TooGoodToGo* in the Integration tab of HACS
1. Click *Install*
1. Restart the Home Assistant server
- ⚠ Each time you add/remove a favorite in the TGTG app, **restart your Home Assistant**. Favorites are only updated at boot!

## Configuration option

```yaml
sensor:
- platform: tgtg

# Optional: email so you know which account is used
email: "Your TGTG mail"

# Mandatory: tokens for authentication - see the tgtg_get_tokens.py script
access_token: "abc123"
refresh_token: "abc123"
user_id: "123456"
cookie: "datadome=..."

# Optional: Refresh the stock every 15 minutes
scan_interval: 900
1. Go to *Configuration* -> *Devices & Services* and setup a new TGTG integration using your email

# Optional, use defined items ID instead to get your favorites
item:
# item_id 1
- 1234
# item_id 2
- 5678

# Optional: user agent - by default, the latest one is retrieved from the Google Play store
#user_agent: "TGTG/22.2.1 Dalvik/2.1.0 (Linux; U; Android 9; SM-G955F Build/PPR1.180610.011)"
```

`access_token`, `refresh_token`, `user_id` and `cookie` can be retrieved using the [tgtg_get_tokens](./tgtg_get_tokens.py) script!

### How to get item_id

Check the [tgtg_get_favorites_item_id](./tgtg_get_favorites_item_id.py) script!

1. Set up email/password
1. Run it
1. Copy the full output in the `configuration.yaml` for the `item` section
## Features

## Q&A
* No Docker-Container needed!!!
* No local Python-Scripts and knowledge needed!!!
* Fetch each item stock defined
* ConfigFlow for easy configuration
* Retrieve all favorites
* Retrieve additional information as attributes, if available:
* Item ID
* Store ID
* TooGoodToGo price and original value
* Pick-up start and end
* Sold-out date
* Sales Window
* Store Logo URL

## How is it polling the data

- This integration is polling all favourites every 15 minutes.
- Every 2 hours the details of an item (for every item in favourites list) are fetched to update the saleswindow and/or pickup dates and other data of the item
- If an item is inside his saleswindow (from start of saleswindow till 10 minutes later) it will be fetched more frequently (every 3 minutes)

- It was working before, but now all TooGoodToGo sensors are "not available"
- Try to update your tokens using [the script](https://github.com/Chouffy/home_assistant_tgtg/blob/main/tgtg_get_tokens.py) and restart Home Assistant
- I have a sensor that shows now as unavailable when there's no stock
- Try add it manually using Item ID - See [this issue](https://github.com/Chouffy/home_assistant_tgtg/issues/18)
- The `tgtg` integration won't start, all my sensors are unavailable and I have a list of manually defined items ID
- Double-check if all items ID defined manually are correct. The integration [don't support unknown or incorrect item ID - see issue](https://github.com/Chouffy/home_assistant_tgtg/issues/22).
62 changes: 62 additions & 0 deletions custom_components/tgtg/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,63 @@
"""The tgtg component."""

import logging

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .client import Client
from .const import (
DOMAIN,
CONF_ACCESS_TOKEN,
CONF_REFRESH_TOKEN,
CONF_USER_ID,
CONF_COOKIE,
TGTG_NAME,
TGTG_CLIENT,
TGTG_COORDINATOR,
DEFAULT_SCAN_INTERVAL
)

LOGGER = logging.getLogger(__name__)

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up TGTG as config entry."""

LOGGER.info(f"Initializing {DOMAIN}")

# Load values from settings
email = entry.data.get(CONF_EMAIL)
access_token = entry.data.get(CONF_ACCESS_TOKEN)
refresh_token = entry.data.get(CONF_REFRESH_TOKEN)
user_id = entry.data.get(CONF_USER_ID)
cookie = entry.data.get(CONF_COOKIE)

# Log in with tokens
tgtg_client = Client(hass=hass, email=email, access_token=access_token, refresh_token=refresh_token, user_id=user_id, cookie=cookie)

tgtg_coordinator = DataUpdateCoordinator(
hass,
LOGGER,
name=f"{DOMAIN} Coordinator for {user_id}",
update_method=tgtg_client.update,
update_interval=DEFAULT_SCAN_INTERVAL,
)

# Fetch initial data so we have data when entities subscribe
await tgtg_coordinator.async_refresh()

# Save the data
tgtg_hass_data = hass.data.setdefault(DOMAIN, {})
tgtg_hass_data[entry.entry_id] = {
TGTG_CLIENT: tgtg_client,
TGTG_COORDINATOR: tgtg_coordinator,
TGTG_NAME: user_id,
}

hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, Platform.SENSOR)
)

return True
106 changes: 106 additions & 0 deletions custom_components/tgtg/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import logging
import datetime

from tgtg import TgtgClient

from .const import CONF_NEXT_SALES_WINDOW, CONF_ITEM, CONF_ITEM_ID

LOGGER = logging.getLogger(__name__)

class Client:
updateCycle = None
items = None

def __init__(self, hass, access_token=None, refresh_token=None, user_id=None, user_agent=None, email=None, cookie=None):
self.hass = hass

if email != "":
self.email = email

if access_token != "":
self.access_token = access_token

if refresh_token != "":
self.refresh_token = refresh_token

if user_id != "":
self.user_id = user_id

if user_agent != "":
self.user_agent = user_agent

if cookie != "":
self.cookie = cookie

if((self.access_token and self.refresh_token and self.user_id and self.user_agent) or self.email):
self.tgtg = TgtgClient(
email=self.email,
access_token=self.access_token,
refresh_token=self.refresh_token,
user_id=self.user_id,
user_agent=self.user_agent,
cookie=self.cookie
)

async def fetch_credentials(self):
return await self.hass.async_add_executor_job(self.tgtg.get_credentials)

async def fetch_favourites(self):
return await self.hass.async_add_executor_job(self.tgtg.get_favourites)

async def fetch_items(self):
return await self.hass.async_add_executor_job(self.tgtg.get_items)

async def fetch_item(self, item_id):
return await self.hass.async_add_executor_job(self.tgtg.get_item, item_id)

def get_item(self, item_id):
return self.items.get(item_id)

async def update_item_details(self, item_id):
LOGGER.debug('Updating item details: %s', item_id)
item = await self.fetch_item(item_id)
# merge data
self.items[item_id] = {**self.items[item_id], **item}

async def update(self):
# update all favourites every 15 minutes
if self.updateCycle is None or self.updateCycle % 5 == 0:
LOGGER.debug('Updating TGTG favourites ...')
items = await self.fetch_items()
self.items = {d[CONF_ITEM][CONF_ITEM_ID]: d for d in items}

# update details of each item
for item_id in self.items:
# update item more often if item is in saleswindow
if self.is_during_sales_window(self.items[item_id], 10):
LOGGER.debug('Updating item details because in saleswindow...')
self.update_item_details(item_id)
# fetch item in detail to get more data (but not so often) = 40 * DEFAULT_SCAN_INTERVAL
elif self.updateCycle is None or self.updateCycle >= 40:
await self.update_item_details(item_id)

# reset updateCycle
if self.updateCycle is None or self.updateCycle >= 40:
self.updateCycle = 0

self.updateCycle += 1

def is_during_sales_window(self, item, salesWindowMinutes):
if CONF_NEXT_SALES_WINDOW in item:
current_datetime = None
sales_window = None

try:
current_datetime = datetime.datetime.now(datetime.timezone.utc)
except ValueError as e:
LOGGER.error('Current Date Error: %s', e)

try:
sales_window = datetime.datetime.strptime(item[CONF_NEXT_SALES_WINDOW], '%Y-%m-%dT%H:%M:%S%z')
except ValueError as e:
LOGGER.error('Current SalesWindow Date Error: %s', e)

return sales_window <= current_datetime <= sales_window + datetime.timedelta(minutes=salesWindowMinutes)

return False
49 changes: 49 additions & 0 deletions custom_components/tgtg/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import voluptuous as vol
import logging

from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL

from .const import DOMAIN, CONF_USER_ID, CONF_COOKIE, CONF_ACCESS_TOKEN, CONF_REFRESH_TOKEN
from .client import Client

LOGGER = logging.getLogger(__name__)

class TGTGConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""TGTG config flow."""

VERSION = 1

async def async_step_user(self, user_input=None):
errors = {}
if user_input is not None:
email = user_input[CONF_EMAIL]

# use email as unique_id
await self.async_set_unique_id(email)
self._abort_if_unique_id_configured()

try:
# Get credentials
tgtg = Client(hass=self.hass, email=email)
data = await tgtg.fetch_credentials()

config = {
CONF_EMAIL: email,
CONF_ACCESS_TOKEN: data["access_token"],
CONF_REFRESH_TOKEN: data["refresh_token"],
CONF_USER_ID: data["user_id"],
CONF_COOKIE: data["cookie"]
}

return self.async_create_entry(title=f"TGTG {email}", data=config)

except Exception as e:
errors["base"] = "Error: {}".format(e)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({
vol.Required(CONF_EMAIL): str
}),
errors=errors
)
42 changes: 42 additions & 0 deletions custom_components/tgtg/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Constants for the Divera integration."""
from datetime import timedelta
from typing import Final

DOMAIN: Final = "tgtg"
CONF_FAVOURITES: Final = "favourites"
CONF_ITEM: Final = "item"
CONF_ITEM_ID: Final = "item_id"
CONF_PRICE_INCL_TAX: Final = "price_including_taxes"
CONF_VALUE_INCL_TAX: Final = "value_including_taxes"
CONF_ITEM_START: Final = "start"
CONF_ITEM_END: Final = "end"
CONF_ITEM_LOGO_PICTURE: Final = "logo_picture"
CONF_PICKUP_INTERVAL: Final = "pickup_interval"
CONF_SOLD_OUT_AT: Final = "sold_out_at"
CONF_NEXT_SALES_WINDOW: Final = "next_sales_window_purchase_start"
CONF_ACCESS_TOKEN: Final = "access_token"
CONF_REFRESH_TOKEN: Final = "refresh_token"
CONF_USER_EMAIL: Final = "email"
CONF_USER_ID: Final = "user_id"
CONF_COOKIE: Final = "cookie"
CONF_USER_AGENT: Final = "user_agent"
CONF_STORE: Final = "store"
CONF_STORE_ID: Final = "store_id"
ATTR_ITEM_ID: Final = "item_id"
ATTR_ITEM_ID_URL: Final = "item_url"
ATTR_STORE_ID: Final = "store_id"
ATTR_PRICE: Final = "price"
ATTR_VALUE: Final = "value"
ATTR_PICKUP_START: Final = "pickup_start"
ATTR_PICKUP_STOP: Final = "pickup_stop"
ATTR_SOLDOUT_DATE: Final = "soldout_date"
ATTR_NEXT_SALES_WINDOW_DATE: Final = "next_saleswindow"
ATTR_LOGO_PICTURE:Final = "store_logo_url"

TGTG_NAME = "tgtg_name"
TGTG_CLIENT = "tgtg_client"
TGTG_COORDINATOR = "tgtg_coordinator"

DEFAULT_SHORT_NAME = "TGTG Store"

DEFAULT_SCAN_INTERVAL = timedelta(minutes=3)
Loading

0 comments on commit fca49e9

Please sign in to comment.