Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

6.0.0 Rewrite with config_flow #69

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 40 additions & 85 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,100 +3,55 @@
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._
## Usage

You only need Docker installed. There is no need to clone the repo because Docker can build from an external URL.
### Installation via [HACS](https://hacs.xyz/)

```
docker build https://github.com/Chouffy/home_assistant_tgtg.git#main --tag "homeassistant_tgtg_tokens:latest"
docker run --rm -it homeassistant_tgtg_tokens
```

### 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!
1. Go to *Configuration* -> *Devices & Services* and setup a new TGTG integration using your email

## Configuration option
## Features

```yaml
sensor:
- platform: tgtg
* 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

# Optional: email so you know which account is used
email: "Your TGTG mail"
## How is it polling the data

# Mandatory: tokens for authentication - see the tgtg_get_tokens.py script
access_token: "abc123"
refresh_token: "abc123"
user_id: "123456"
cookie: "datadome=..."
- 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)

# Optional: Refresh the stock every 15 minutes
scan_interval: 900
## Example Automation

# Optional, use defined items ID instead to get your favorites
item:
# item_id 1
- 1234
# item_id 2
- 5678
As example we'll create a notification using the HomeAssistant Companion App on your smartphone.

# 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)"
```
alias: TGTG Q1 Notification
description: ""
trigger:
- platform: numeric_state
entity_id: sensor.schaal_mehr_als_tanken_q1_tankstelle_karlsruhe_uberraschungstute
above: 0
condition: []
action:
- service: notify.notify
data:
message: Q1 gas station has new packages available
title: TGTG Notification
mode: single
```

`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

## Q&A

- 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).
As soon as the number of your entry is higher than 0, this is immediately sent to you via push notification to the Companion App and displayed on your cell phone.
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
124 changes: 124 additions & 0 deletions custom_components/tgtg/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import logging
import datetime
import time

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, *args, **kwargs):
return await self.hass.async_add_executor_job(lambda: self.tgtg.get_items(*args, **kwargs))

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

# fetch_items per page until result == 0 (to get all pages)
async def fetch_all_items(self, *args, **kwargs):
items_list = {}
page = 1
while True:
LOGGER.debug('Fetching page: %d', page)
extended_kwargs = {'page': page, **kwargs}
items = await self.fetch_items(*args, **extended_kwargs)
page += 1
if len(items) == 0:
break
for d in items:
items_list[d[CONF_ITEM][CONF_ITEM_ID]] = d
return items_list

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 ...')
self.items = await self.fetch_all_items()
LOGGER.info('Got %d favourites', len(self.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)
time.sleep(0.5) # sleep for 500ms to not flood tgtg api
# 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)
time.sleep(0.5) # sleep for 500ms to not flood tgtg api

# 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
)
Loading