work in progress, playing around

This commit is contained in:
vabene1111
2022-10-02 16:39:56 +02:00
parent da8262a9b5
commit bc82049f42
8 changed files with 192 additions and 10 deletions

View File

@@ -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>

View File

View 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

View 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),
),
]

View File

@@ -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)

View File

@@ -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 %}

View File

@@ -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?

View File

@@ -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})