mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-23 18:29:23 -05:00
work in progress, playing around
This commit is contained in:
2
.idea/dictionaries/vabene1111_PC.xml
generated
2
.idea/dictionaries/vabene1111_PC.xml
generated
@@ -3,6 +3,8 @@
|
|||||||
<words>
|
<words>
|
||||||
<w>autosync</w>
|
<w>autosync</w>
|
||||||
<w>chowdown</w>
|
<w>chowdown</w>
|
||||||
|
<w>cookingmachine</w>
|
||||||
|
<w>cookit</w>
|
||||||
<w>csrftoken</w>
|
<w>csrftoken</w>
|
||||||
<w>gunicorn</w>
|
<w>gunicorn</w>
|
||||||
<w>ical</w>
|
<w>ical</w>
|
||||||
|
|||||||
0
cookbook/cooking_machines/__init__.py
Normal file
0
cookbook/cooking_machines/__init__.py
Normal file
116
cookbook/cooking_machines/homeconnect_cookit.py
Normal file
116
cookbook/cooking_machines/homeconnect_cookit.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import json
|
||||||
|
import random
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
from cookbook.models import CookingMachine
|
||||||
|
|
||||||
|
|
||||||
|
# Tandoor is not affiliated in any way or form with the holders of Trademark or other right associated with the mentioned names. All mentioned protected names are purely used to identify to the user a certain device or integration.
|
||||||
|
class HomeConnectCookit:
|
||||||
|
AUTH_AUTHORIZE_URL = 'https://api.home-connect.com/security/oauth/authorize'
|
||||||
|
AUTH_TOKEN_URL = 'https://api.home-connect.com/security/oauth/token'
|
||||||
|
AUTH_REFRESH_URL = 'https://api.home-connect.com/security/oauth/token'
|
||||||
|
|
||||||
|
RECIPE_API_URL = 'https://prod.reu.rest.homeconnectegw.com/user-generated-recipes/server/api/v1/recipes'
|
||||||
|
IMAGE_API_URL = 'https://prod.reu.rest.homeconnectegw.com/user-generated-recipes/server/api/v1/images'
|
||||||
|
|
||||||
|
CLIENT_ID = '' # TODO load from .env settings
|
||||||
|
_CLIENT_SECRET = '' # TODO load from .env settings
|
||||||
|
|
||||||
|
_cooking_machine = None
|
||||||
|
|
||||||
|
def __init__(self, cooking_machine):
|
||||||
|
self._cooking_machine = cooking_machine
|
||||||
|
|
||||||
|
def get_auth_link(self):
|
||||||
|
return f"{self.AUTH_AUTHORIZE_URL}?client_id={self.CLIENT_ID}&response_type=code&scope=IdentifyAppliance%20Settings&state={random.randint(100000, 999999)}"
|
||||||
|
|
||||||
|
def _validate_token(self):
|
||||||
|
if self._cooking_machine.access_token is None and self._cooking_machine.refresh_token is None:
|
||||||
|
return False # user needs to login
|
||||||
|
elif self._cooking_machine.access_token_expiry < now() + timedelta(minutes=10):
|
||||||
|
return False # refresh token
|
||||||
|
|
||||||
|
def _refresh_access_token(self):
|
||||||
|
token_response = requests.post(self.AUTH_REFRESH_URL, {
|
||||||
|
'grant_type': 'refresh_token',
|
||||||
|
'refresh_token': self._cooking_machine.refresh_token,
|
||||||
|
'client_secret': self._CLIENT_SECRET,
|
||||||
|
})
|
||||||
|
if token_response.status_code == 200:
|
||||||
|
token_response_body = json.loads(token_response.content)
|
||||||
|
self._cooking_machine.access_token = token_response_body['access_token']
|
||||||
|
self._cooking_machine.access_token_expiry = now() + timedelta(seconds=(token_response_body['expires_in'] - (60 * 10)))
|
||||||
|
self._cooking_machine.refresh_token = token_response_body['refresh_token']
|
||||||
|
self._cooking_machine.refresh_token_expiry = now() + timedelta(days=58)
|
||||||
|
self._cooking_machine.save()
|
||||||
|
|
||||||
|
def get_access_token(self, code):
|
||||||
|
token_response = requests.post(self.AUTH_TOKEN_URL, {
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'code': code,
|
||||||
|
'client_id': self.CLIENT_ID,
|
||||||
|
'client_secret': self._CLIENT_SECRET,
|
||||||
|
})
|
||||||
|
if token_response.status_code == 200:
|
||||||
|
token_response_body = json.loads(token_response.content)
|
||||||
|
self._cooking_machine.access_token = token_response_body['access_token']
|
||||||
|
self._cooking_machine.access_token_expiry = now() + timedelta(seconds=(token_response_body['expires_in'] - (60 * 10)))
|
||||||
|
self._cooking_machine.refresh_token = token_response_body['refresh_token']
|
||||||
|
self._cooking_machine.refresh_token_expiry = now() + timedelta(days=58)
|
||||||
|
self._cooking_machine.save()
|
||||||
|
|
||||||
|
def _get_default_headers(self):
|
||||||
|
auth_token = ''
|
||||||
|
return {
|
||||||
|
'authorization': f'Bearer {self._cooking_machine.access_token}',
|
||||||
|
'accept-language': "en-US",
|
||||||
|
"referer": "https://prod.reu.rest.homeconnectegw.com/user-generated- recipes/client/editor/recipedetails",
|
||||||
|
"user-agent": "Mozilla/5.0 (Linux; Android 10; Android SDK built for x86 Build/QSR1.210802.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.185 Mobile Safari/537.36",
|
||||||
|
"x-requested-with": "com.bshg.homeconnect.android.release"
|
||||||
|
}
|
||||||
|
|
||||||
|
def push_recipe(self, recipe):
|
||||||
|
data = {
|
||||||
|
"title": recipe.name,
|
||||||
|
"description": recipe.description,
|
||||||
|
"ingredients": [],
|
||||||
|
"steps": [],
|
||||||
|
"durations": {
|
||||||
|
"totalTime": (recipe.working_time + recipe.waiting_time) * 60,
|
||||||
|
"cookingTime": 0, # recipe.waiting_time * 60, #TODO cooking time must be sum of step duration attribute, otherwise creation fails
|
||||||
|
"preparationTime": recipe.working_time * 60,
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"servings": {
|
||||||
|
"amount": int(recipe.servings),
|
||||||
|
"unit": 'portion', # required
|
||||||
|
},
|
||||||
|
"servingTipsStep": {
|
||||||
|
"textInstructions": "Serve"
|
||||||
|
},
|
||||||
|
"complexityLevel": "medium",
|
||||||
|
"image": {
|
||||||
|
"url": "https://media3.bsh-group.com/Recipes/800x480/17062805_210217_My-own-recipe_Picture_188ppi.jpg",
|
||||||
|
"mimeType": "image/jpeg"
|
||||||
|
# TODO add image upload
|
||||||
|
},
|
||||||
|
"accessories": [], # TODO add once tandoor supports tools
|
||||||
|
"legalDisclaimerApproved": True # TODO force user to approve disclaimer
|
||||||
|
}
|
||||||
|
for step in recipe.steps.all():
|
||||||
|
data['steps'].append({
|
||||||
|
"textInstructions": step.instruction
|
||||||
|
})
|
||||||
|
for i in step.ingredients.all():
|
||||||
|
data['ingredients'].append({
|
||||||
|
"amount": int(i.amount),
|
||||||
|
"unit": i.unit.name,
|
||||||
|
"name": i.food.name,
|
||||||
|
})
|
||||||
|
# TODO create synced recipe
|
||||||
|
response = requests.post(f'{self.RECIPE_API_URL}', json=data, headers=self._get_default_headers())
|
||||||
|
return response
|
||||||
31
cookbook/migrations/0185_cookingmachine.py
Normal file
31
cookbook/migrations/0185_cookingmachine.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 4.0.7 on 2022-10-02 11:51
|
||||||
|
|
||||||
|
import cookbook.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cookbook', '0184_alter_userpreference_image'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CookingMachine',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('type', models.CharField(choices=[('HOMECONNECT_COOKIT', 'HomeConnect CookIt')], max_length=128)),
|
||||||
|
('name', models.CharField(max_length=128)),
|
||||||
|
('serial', models.CharField(blank=True, max_length=512, null=True)),
|
||||||
|
('description', models.TextField(blank=True, default='')),
|
||||||
|
('access_token', models.CharField(blank=True, max_length=4096, null=True)),
|
||||||
|
('access_token_expiry', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('refresh_token', models.CharField(blank=True, max_length=4096, null=True)),
|
||||||
|
('refresh_token_expiry', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
|
||||||
|
],
|
||||||
|
bases=(models.Model, cookbook.models.PermissionModelMixin),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -366,7 +366,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||||
image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True,blank=True, related_name='user_image')
|
image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='user_image')
|
||||||
theme = models.CharField(choices=THEMES, max_length=128, default=TANDOOR)
|
theme = models.CharField(choices=THEMES, max_length=128, default=TANDOOR)
|
||||||
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
|
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
|
||||||
default_unit = models.CharField(max_length=32, default='g')
|
default_unit = models.CharField(max_length=32, default='g')
|
||||||
@@ -1010,6 +1010,27 @@ def default_valid_until():
|
|||||||
return date.today() + timedelta(days=14)
|
return date.today() + timedelta(days=14)
|
||||||
|
|
||||||
|
|
||||||
|
class CookingMachine(models.Model, PermissionModelMixin):
|
||||||
|
# Tandoor is not affiliated in any way or form with the holders of Trademark or other right associated with the mentioned names. All mentioned protected names are purely used to identify to the user a certain device or integration.
|
||||||
|
HOMECONNECT_COOKIT = 'HOMECONNECT_COOKIT'
|
||||||
|
MACHINE_TYPES = (
|
||||||
|
(HOMECONNECT_COOKIT, _('HomeConnect CookIt')),
|
||||||
|
)
|
||||||
|
|
||||||
|
type = models.CharField(choices=MACHINE_TYPES, max_length=128)
|
||||||
|
name = models.CharField(max_length=128)
|
||||||
|
serial = models.CharField(max_length=512, null=True, blank=True)
|
||||||
|
description = models.TextField(default='', blank=True)
|
||||||
|
|
||||||
|
access_token = models.CharField(max_length=4096, null=True, blank=True)
|
||||||
|
access_token_expiry = models.DateTimeField(null=True, blank=True)
|
||||||
|
refresh_token = models.CharField(max_length=4096, null=True, blank=True)
|
||||||
|
refresh_token_expiry = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
|
|
||||||
class InviteLink(ExportModelOperationsMixin('invite_link'), models.Model, PermissionModelMixin):
|
class InviteLink(ExportModelOperationsMixin('invite_link'), models.Model, PermissionModelMixin):
|
||||||
uuid = models.UUIDField(default=uuid.uuid4)
|
uuid = models.UUIDField(default=uuid.uuid4)
|
||||||
email = models.EmailField(blank=True)
|
email = models.EmailField(blank=True)
|
||||||
|
|||||||
@@ -10,6 +10,10 @@
|
|||||||
|
|
||||||
{% block content_fluid %}
|
{% block content_fluid %}
|
||||||
|
|
||||||
|
<a href="{{ login_url }}" target="_blank" rel="noreferrer nofollow">Login CookIt</a>
|
||||||
|
<br/>
|
||||||
|
<a href="?sync=748" >Sync</a>
|
||||||
|
|
||||||
{{ data }}
|
{{ data }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ urlpatterns = [
|
|||||||
path('api/download-file/<int:file_id>/', api.download_file, name='api_download_file'),
|
path('api/download-file/<int:file_id>/', api.download_file, name='api_download_file'),
|
||||||
|
|
||||||
|
|
||||||
|
path('cooking-machine/auth/homeconnect/', views.test2, name='cookingmachine_auth_homeconnect'),
|
||||||
|
|
||||||
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
|
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
|
||||||
# TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
|
# TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
|
||||||
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this deprecated?
|
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this deprecated?
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ from django.utils.translation import gettext as _
|
|||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from oauth2_provider.models import AccessToken
|
from oauth2_provider.models import AccessToken
|
||||||
|
|
||||||
|
from cookbook.cooking_machines.homeconnect_cookit import HomeConnectCookit
|
||||||
from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, ShoppingPreferenceForm,
|
from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, ShoppingPreferenceForm,
|
||||||
SpaceCreateForm, SpaceJoinForm, User,
|
SpaceCreateForm, SpaceJoinForm, User,
|
||||||
UserCreateForm, UserNameForm, UserPreference, UserPreferenceForm)
|
UserCreateForm, UserNameForm, UserPreference, UserPreferenceForm)
|
||||||
from cookbook.helper.permission_helper import group_required, has_group_permission, share_link_valid, switch_user_active_space
|
from cookbook.helper.permission_helper import group_required, has_group_permission, share_link_valid, switch_user_active_space
|
||||||
from cookbook.models import (Comment, CookLog, InviteLink, SearchFields, SearchPreference, ShareLink,
|
from cookbook.models import (Comment, CookLog, InviteLink, SearchFields, SearchPreference, ShareLink,
|
||||||
Space, ViewLog, UserSpace)
|
Space, ViewLog, UserSpace, CookingMachine)
|
||||||
from cookbook.tables import (CookLogTable, ViewLogTable)
|
from cookbook.tables import (CookLogTable, ViewLogTable)
|
||||||
from recipes.version import BUILD_REF, VERSION_NUMBER
|
from recipes.version import BUILD_REF, VERSION_NUMBER
|
||||||
|
|
||||||
@@ -434,17 +435,22 @@ def test(request):
|
|||||||
if not settings.DEBUG:
|
if not settings.DEBUG:
|
||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
|
|
||||||
from cookbook.helper.ingredient_parser import IngredientParser
|
machine = CookingMachine.objects.get_or_create(type=CookingMachine.HOMECONNECT_COOKIT, name='Test', space=request.space)[0]
|
||||||
parser = IngredientParser(request, False)
|
test = HomeConnectCookit(machine)
|
||||||
|
|
||||||
data = {
|
return render(request, 'test.html', {'login_url': test.get_auth_link()})
|
||||||
'original': '1 Porreestange(n) , ca. 200 g'
|
|
||||||
}
|
|
||||||
data['parsed'] = parser.parse(data['original'])
|
|
||||||
|
|
||||||
return render(request, 'test.html', {'data': data})
|
|
||||||
|
|
||||||
|
|
||||||
def test2(request):
|
def test2(request):
|
||||||
if not settings.DEBUG:
|
if not settings.DEBUG:
|
||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
|
|
||||||
|
machine = CookingMachine.objects.get_or_create(type=CookingMachine.HOMECONNECT_COOKIT, name='Test', space=request.space)[0]
|
||||||
|
test = HomeConnectCookit(machine)
|
||||||
|
if 'code' in request.GET:
|
||||||
|
test.get_access_token(request.GET['code'])
|
||||||
|
|
||||||
|
if 'sync' in request.GET:
|
||||||
|
result = test.push_recipe(Recipe.objects.get(pk=request.GET['sync'], space=request.space))
|
||||||
|
|
||||||
|
return render(request, 'test.html', {'data': request})
|
||||||
|
|||||||
Reference in New Issue
Block a user