From 86240b539470c530fcb5ec5c25af2da39f3f9f03 Mon Sep 17 00:00:00 2001
From: fejao <mail@fejao.de>
Date: Wed, 5 Mar 2025 17:14:50 +0100
Subject: [PATCH 1/6] Added .gitignore

---
 .gitignore | 3 +++
 1 file changed, 3 insertions(+)
 create mode 100644 .gitignore

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fae8b69
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*.venv*
+*OLD*
+*fejao_notes*
-- 
GitLab


From e5526fbd641caffd821191e35bcfd5de626c3121 Mon Sep 17 00:00:00 2001
From: fejao <mail@fejao.de>
Date: Wed, 5 Mar 2025 17:15:25 +0100
Subject: [PATCH 2/6] Updated .gitignore file

---
 .gitignore | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitignore b/.gitignore
index fae8b69..1e8df78 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 *.venv*
+*__pycache__*
 *OLD*
 *fejao_notes*
-- 
GitLab


From 430f74476d928206b0c4bed8d949e276e3702634 Mon Sep 17 00:00:00 2001
From: fejao <mail@fejao.de>
Date: Wed, 5 Mar 2025 17:15:55 +0100
Subject: [PATCH 3/6] Added django

---
 django/api/__init__.py                        |   0
 django/api/apps.py                            |  12 +
 django/api/urls.py                            |  13 +
 django/api/views.py                           | 442 ++++++++++++++++++
 django/api_admin/__init__.py                  |   0
 django/api_admin/apps.py                      |  12 +
 django/api_admin/urls.py                      |  13 +
 django/api_admin/views.py                     |  76 +++
 django/api_auth/__init__.py                   |   0
 django/api_auth/apps.py                       |  12 +
 django/api_auth/templates/login.html          |  24 +
 django/api_auth/templates/logout.html         |  14 +
 django/api_auth/urls.py                       |  22 +
 django/api_auth/views.py                      |  97 ++++
 django/backend/__init__.py                    |   0
 django/backend/asgi.py                        |  16 +
 django/backend/settings.py                    | 212 +++++++++
 django/backend/urls.py                        |  55 +++
 django/backend/wsgi.py                        |  16 +
 django/manage.py                              |  22 +
 django/requirements.txt                       |  10 +
 django/shared_models/__init__.py              |   0
 django/shared_models/apps.py                  |  12 +
 .../shared_models/migrations/0001_initial.py  |  38 ++
 django/shared_models/migrations/__init__.py   |   0
 django/shared_models/models.py                |  49 ++
 django/shared_models/serializers.py           |  41 ++
 27 files changed, 1208 insertions(+)
 create mode 100644 django/api/__init__.py
 create mode 100644 django/api/apps.py
 create mode 100644 django/api/urls.py
 create mode 100644 django/api/views.py
 create mode 100644 django/api_admin/__init__.py
 create mode 100644 django/api_admin/apps.py
 create mode 100644 django/api_admin/urls.py
 create mode 100644 django/api_admin/views.py
 create mode 100644 django/api_auth/__init__.py
 create mode 100644 django/api_auth/apps.py
 create mode 100644 django/api_auth/templates/login.html
 create mode 100644 django/api_auth/templates/logout.html
 create mode 100644 django/api_auth/urls.py
 create mode 100644 django/api_auth/views.py
 create mode 100644 django/backend/__init__.py
 create mode 100644 django/backend/asgi.py
 create mode 100644 django/backend/settings.py
 create mode 100644 django/backend/urls.py
 create mode 100644 django/backend/wsgi.py
 create mode 100755 django/manage.py
 create mode 100644 django/requirements.txt
 create mode 100644 django/shared_models/__init__.py
 create mode 100644 django/shared_models/apps.py
 create mode 100644 django/shared_models/migrations/0001_initial.py
 create mode 100644 django/shared_models/migrations/__init__.py
 create mode 100644 django/shared_models/models.py
 create mode 100644 django/shared_models/serializers.py

diff --git a/django/api/__init__.py b/django/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django/api/apps.py b/django/api/apps.py
new file mode 100644
index 0000000..f1d64a8
--- /dev/null
+++ b/django/api/apps.py
@@ -0,0 +1,12 @@
+"""
+    Default setted file from django for an added app.
+"""
+
+from django.apps import AppConfig
+
+
+class ApiConfig(AppConfig):
+    """Default setted SharedModelsConfig class for the app.
+    """
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "api"
diff --git a/django/api/urls.py b/django/api/urls.py
new file mode 100644
index 0000000..592c29f
--- /dev/null
+++ b/django/api/urls.py
@@ -0,0 +1,13 @@
+"""
+    File for setting the URLs used from the api django app.
+"""
+
+from django.urls import path
+from . import views
+
+
+urlpatterns = [
+    path('box/search', views.ButtonBoxSearchView.as_view(), name='box_search'),
+    path('box/use', views.ButtonBoxUseView.as_view(), name='box_use'),
+    path('box/use/count', views.ButtonBoxUseCountView.as_view(), name='box_use_count'),
+]
diff --git a/django/api/views.py b/django/api/views.py
new file mode 100644
index 0000000..aaeea0d
--- /dev/null
+++ b/django/api/views.py
@@ -0,0 +1,442 @@
+"""
+    File for setting the view classes used from the api django app.
+"""
+
+from datetime import datetime, timezone
+
+from shared_models.models import ButtonBox, ButtonBoxUse
+from shared_models.serializers import ButtonBoxSerializer, ButtonBoxUseSerializer
+
+from rest_framework import status
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from rest_framework.renderers import JSONRenderer
+from rest_framework_simplejwt.authentication import JWTAuthentication
+
+
+###
+### HELPERS
+###
+def helper_get_from_parsed(parsed_name: str, parsed_value: str, parsed_type: str):
+    """Helper method used from the ButtonBoxUseView class for setting the return values from different 'parsed_name' and 'parsed_type'.
+    It will fetch or update the values from the ButtonBoxUse model.
+
+    Parameters
+    ----------
+    parsed_name: str
+        String with the name parsed.
+    parsed_value: str
+        String with the value parsed.
+    parsed_type: str
+        String with the type parsed.
+
+    Returns
+    -------
+    ret_dict: dict
+        The Dictionary with the output from executing this helper method.
+    ret_status: rest_framework.status
+        The HTTP status from executing this helper method.
+    """
+
+    founded_box = None
+    ret_dict = None
+    ret_status = status.HTTP_500_INTERNAL_SERVER_ERROR
+    try:
+        if parsed_name == "id":
+            founded_box = ButtonBox.objects.get(id=parsed_value)
+
+        elif parsed_name == "name":
+            founded_box = ButtonBox.objects.get(name=parsed_value)
+
+        elif parsed_name == "pin_button":
+            founded_box = ButtonBox.objects.get(pin_button=parsed_value)
+
+        elif parsed_name == "pin_led":
+            founded_box = ButtonBox.objects.get(pin_led=parsed_value)
+
+        else:
+            ret_dict = {
+                'error': True,
+                'error_msg': f"ERROR: unknown parameter parsed: {parsed_name}."
+            }
+            ret_status = status.HTTP_406_NOT_ACCEPTABLE
+
+    except ButtonBox.DoesNotExist:
+        ret_dict = {
+            'error': True,
+            'error_msg': f"No Box could be founded with the '{parsed_name}': {parsed_value}"
+        }
+        ret_status = status.HTTP_404_NOT_FOUND
+
+    except Exception as e:
+        ret_dict = {
+            'error': True,
+            'error_msg': f"ERROR happend when trying to search for the box with {parsed_name}: {parsed_value} | Error: {e}"
+        }
+        ret_status = status.HTTP_406_NOT_ACCEPTABLE
+
+    ### If error ocurred, finish
+    if ret_dict is not None:
+        pass
+
+    else:
+        if parsed_type == "add":
+            try:
+                ### Add pushed from pin
+                added = ButtonBoxUse(box_used=founded_box)
+                added.save()
+
+                ret_dict = {
+                    'error': False,
+                    'message': "Added button press",
+                    "button-used": {
+                        "id": added.id,
+                        "box_id": added.box_used.id,
+                        'box_name': added.box_used.name,
+                        'box_pin_button': added.box_used.pin_button,
+                        'box_pin_led': added.box_used.pin_led,
+                        'date': str(added.date)
+                    }
+                }
+                ret_status = status.HTTP_200_OK
+
+
+            except Exception as e:
+                ret_dict = {
+                    'error': True,
+                    'message': f"Error saving the press button with output: {e}",
+                }
+                ret_status = status.HTTP_418_IM_A_TEAPOT
+
+        elif parsed_type == "list":
+            try:
+                ### Get list pushed button
+                box_uses = ButtonBoxUse.objects.filter(box_used=founded_box)
+                serializer = ButtonBoxUseSerializer(box_uses, many=True)
+                ret_dict = serializer.data
+                ret_status = status.HTTP_200_OK
+
+            except Exception as e:
+                ret_dict = {
+                    'error': True,
+                    'error_msg': f"ERROR happend when trying to search for the box with {parsed_name}: {parsed_value} | Error: {e}"
+                }
+                ret_status = status.HTTP_406_NOT_ACCEPTABLE
+
+        else:
+            ret_dict = {
+                'error': True,
+                'error_msg': "ERROR parsing response from 'helper_get_from_parsed' function"
+            }
+            ret_status = status.HTTP_501_NOT_IMPLEMENTED
+
+    return ret_dict, ret_status
+
+
+###
+### VIEW CLASSES
+###
+class ButtonBoxSearchView(APIView):
+    """Class for setting the API views for Searching at the ButtonBox model.
+    """
+
+    # permission_classes = [IsAuthenticated]
+    authentication_classes = [JWTAuthentication]
+    renderer_classes = [JSONRenderer]
+
+    def get(self, request, format=None):
+        """Method for dealing with the GET requests done at this view.
+
+        Parameters
+        ----------
+        request: rest_framework.request.Request
+            The parameters from the GET request.
+        format: None
+            If some format was sent.
+
+        Returns
+        -------
+        rest_framework.response.Response
+            The response object.
+        """
+        # Get parsed
+        box_id = request.query_params.get("id")
+        box_name = request.query_params.get("name")
+        box_pin_button = request.query_params.get("pin_button")
+        box_pin_led = request.query_params.get("pin_led")
+
+        if box_id is not None and box_name is None and box_pin_button is None and box_pin_led is None:
+            box_founded = ButtonBox.objects.filter(id__icontains=box_id)
+
+        elif box_id is None and box_name is not None and box_pin_button is None and box_pin_led is None:
+            box_founded = ButtonBox.objects.filter(name__icontains=box_name)
+
+        elif box_id is None and box_name is None and box_pin_button is not None and box_pin_led is None:
+            box_founded = ButtonBox.objects.filter(pin_button__icontains=box_pin_button)
+
+        elif box_id is None and box_name is None and box_pin_button is None and box_pin_led is not None:
+            box_founded = ButtonBox.objects.filter(pin_led__icontains=box_pin_led)
+
+        # If not, return all
+        else:
+            box_founded = ButtonBox.objects.all()
+
+        serializer = ButtonBoxSerializer(box_founded, many=True)
+
+        return Response(serializer.data, status=status.HTTP_200_OK)
+
+
+class ButtonBoxUseView(APIView):
+    """Class for setting the API views for Searching and Adding values at the ButtonBoxUse model.
+    """
+
+    # permission_classes = [IsAuthenticated]
+    authentication_classes = [JWTAuthentication]
+    renderer_classes = [JSONRenderer]
+
+    def get(self, request, format=None):
+        """Method for dealing with the GET requests done at this view.
+
+        Parameters
+        ----------
+        request: rest_framework.request.Request
+            The parameters from the GET request.
+        format: None
+            If some format was sent.
+
+        Returns
+        -------
+        rest_framework.response.Response
+            The response object.
+        """
+
+        # Get parsed
+        box_id = request.query_params.get("id")
+        box_name = request.query_params.get("name")
+        box_pin_button = request.query_params.get("pin_button")
+        box_pin_led = request.query_params.get("pin_led")
+
+        if box_id is not None and box_name is None and box_pin_button is None and box_pin_led is None:
+            founded_box = ButtonBox.objects.get(id=box_id)
+
+        elif box_id is None and box_name is not None and box_pin_button is None and box_pin_led is None:
+            founded_box = ButtonBox.objects.get(name=box_name)
+
+        elif box_id is None and box_name is None and box_pin_button is not None and box_pin_led is None:
+            founded_box = ButtonBox.objects.get(pin_button=box_pin_button)
+
+        elif box_id is None and box_name is None and box_pin_button is None and box_pin_led is not None:
+            founded_box = ButtonBox.objects.get(pin_led=box_pin_led)
+
+        else:
+            founded_box = None
+
+        # Get from parsed, if None get all
+        if founded_box is None:
+            boxes_uses = ButtonBoxUse.objects.all()
+            serializer = ButtonBoxUseSerializer(boxes_uses, many=True)
+
+        else:
+            box_uses = ButtonBoxUse.objects.filter(box_used=founded_box)
+            serializer = ButtonBoxUseSerializer(box_uses, many=True)
+
+        return Response(serializer.data, status=status.HTTP_200_OK)
+
+    def post(self, request, format=None):
+        """Method for dealing with the POST requests done at this view.
+
+        Parameters
+        ----------
+        request: rest_framework.request.Request
+            The parameters from the POST request.
+        format: None
+            If some format was sent.
+
+        Returns
+        -------
+        rest_framework.response.Response
+            The response object.
+        """
+        # Get the box name from the query parameters (if Nonne, default to empty str)
+        parsed_id = request.data.get("id")
+        parsed_name = request.data.get("name")
+        # parsed_pin_number = request.data.get("pin_number")
+        parsed_pin_button = request.data.get("pin_button")
+        parsed_pin_led = request.data.get("pin_led")
+
+        if parsed_id is None and parsed_name is None and parsed_pin_button is None and parsed_pin_led is None:
+            ret_dict = {
+                "error": True,
+                "message": "Unable to add box used from this end-point withtout parsed info from 'name', 'pin_button' or 'pin_led'"
+            }
+            ret_status = status.HTTP_400_BAD_REQUEST
+
+        elif parsed_id is not None and parsed_name is not None and parsed_pin_button is not None and parsed_pin_led is not None:
+            ret_dict = {
+                "error": True,
+                "message": "Unable to add box used from this end-point with MULTIPLE parsed infos from 'id', 'name', 'pin_button' and 'pin_led'. Use only one"
+            }
+            ret_status = status.HTTP_400_BAD_REQUEST
+
+        ### ID
+        elif parsed_id is not None and parsed_name is None and parsed_pin_button is None and parsed_pin_led is None:
+            ret_dict, ret_status = helper_get_from_parsed(
+                parsed_name='id',
+                parsed_value=parsed_id,
+                parsed_type='add'
+            )
+
+        ### NAME
+        elif parsed_name is not None and parsed_pin_button is None and parsed_pin_led is None:
+            ret_dict, ret_status = helper_get_from_parsed(
+                parsed_name='name',
+                parsed_value=parsed_name,
+                parsed_type='add'
+            )
+
+        ### PIN_BUTTON
+        elif parsed_name is None and parsed_pin_button is not None and parsed_pin_led is None:
+            ret_dict, ret_status = helper_get_from_parsed(
+                parsed_name='pin_button',
+                parsed_value=parsed_pin_button,
+                parsed_type='add'
+            )
+
+        ### PIN_LED
+        elif parsed_name is None and parsed_pin_button is None and parsed_pin_led is not None:
+            ret_dict, ret_status = helper_get_from_parsed(
+                parsed_name='pin_led',
+                parsed_value=parsed_pin_led,
+                parsed_type='add'
+            )
+
+        else:
+            ret_dict = {
+                "error": True,
+                "message": "Option not implemented..."
+            }
+            ret_status = status.HTTP_501_NOT_IMPLEMENTED
+
+        return Response(ret_dict, status=ret_status)
+
+
+class ButtonBoxUseCountView(APIView):
+    """Class for setting the API views for getting the amount of uses from a Button Box.
+    It is mostly used from Grafana for getting the fancy requests from date windows.
+    """
+
+    # permission_classes = [IsAuthenticated]
+    authentication_classes = [JWTAuthentication]
+    renderer_classes = [JSONRenderer]
+
+    def get(self, request, format=None):
+        """Method for dealing with the GET requests done at this view.
+
+        Parameters
+        ----------
+        request: rest_framework.request.Request
+            The parameters from the GET request.
+        format: None
+            If some format was sent.
+
+        Returns
+        -------
+        rest_framework.response.Response
+            The response object.
+        """
+
+        ###
+        ### EXAMPLE of the grafana used URL ;)
+        ###
+        ### http://localhost:8000/api/box/use/count?from=${__from}&to=${__to}
+        ###
+
+        ### GET PARSED
+        query_param_from = request.query_params.get("from")
+        query_param_to = request.query_params.get("to")
+
+        if query_param_from is not None and query_param_to is None:
+            ret_list = [
+                {
+                    'error': True,
+                    'error_msg': "Query parameter for 'from' is parsed and 'to' is not parsed. You have to use BOUTH or NONE"
+                }
+            ]
+            ret_status = status.HTTP_400_BAD_REQUEST
+
+        elif query_param_from is None and query_param_to is not None:
+            ret_list = [
+                {
+                    'error': True,
+                    'error_msg': "Query parameter for 'to' is parsed and 'from' is not parsed. You have to use BOUTH or NONE"
+                }
+            ]
+            ret_status = status.HTTP_400_BAD_REQUEST
+
+        ### RETURN ALL
+        elif query_param_from is None and query_param_to is None:
+
+            ret_list = []
+            for button_box in ButtonBox.objects.all():
+                ret_list.append(
+                    {
+                        "name": button_box.name,
+                        "count": ButtonBoxUse.objects.filter(box_used=button_box).count()
+                    }
+                )
+
+            ret_status = status.HTTP_200_OK
+
+        ### RETURN FROM TIME FRAME
+        elif query_param_from is not None and query_param_to is not None:
+
+            try:
+                date_from = datetime.fromtimestamp(int(query_param_from) / 1000, timezone.utc)
+
+            except Exception as e:
+                ret_list = [
+                    {
+                        'error': True,
+                        'error_msg': f"Error parsing epoch time to datetime from parsed 'date_from' parameter with error: {e}"
+                    }
+                ]
+                ret_status = status.HTTP_400_BAD_REQUEST
+
+            try:
+                date_to = datetime.fromtimestamp(int(query_param_to) / 1000, timezone.utc)
+
+            except Exception as e:
+                ret_list = [
+                    {
+                        'error': True,
+                        'error_msg': f"Error parsing epoch time to datetime from parsed 'date_to' parameter with error: {e}"
+                    }
+                ]
+                ret_status = status.HTTP_400_BAD_REQUEST
+
+            ret_list = []
+            for button_box in ButtonBox.objects.all():
+                button_name = button_box.name
+                count = 0
+                for button_use in ButtonBoxUse.objects.filter(date__gt=date_from, date__lt=date_to):
+                    if button_name == button_use.box_name:
+                        count += 1
+                        ret_list.append(
+                            {
+                                "name": button_name,
+                                "count": count,
+                                "date": button_use.date
+                            }
+                        )
+            ret_status = status.HTTP_200_OK
+
+        else:
+            ret_list = [
+                {
+                    'error': True,
+                    'error_msg': "Not implemented"
+                }
+            ]
+            ret_status = status.HTTP_501_NOT_IMPLEMENTED
+
+        return Response(ret_list, status=ret_status)
diff --git a/django/api_admin/__init__.py b/django/api_admin/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django/api_admin/apps.py b/django/api_admin/apps.py
new file mode 100644
index 0000000..bc61874
--- /dev/null
+++ b/django/api_admin/apps.py
@@ -0,0 +1,12 @@
+"""
+    Default setted file from django for an added app.
+"""
+
+from django.apps import AppConfig
+
+
+class ApiAdminConfig(AppConfig):
+    """Default setted SharedModelsConfig class for the app.
+    """
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "api_admin"
diff --git a/django/api_admin/urls.py b/django/api_admin/urls.py
new file mode 100644
index 0000000..6301cd4
--- /dev/null
+++ b/django/api_admin/urls.py
@@ -0,0 +1,13 @@
+"""
+    File for setting the URLs used from the api_admin django app.
+"""
+
+from django.urls import path
+from . import views
+
+
+urlpatterns = [
+    path('box', views.AdminButtonBoxListCreateDeleteView.as_view(), name='admin_box'),
+    path('box/edit/<int:pk>', views.AdminButtonBoxRetrieveUpdateDestroyView.as_view(), name='admin_box_edit'),
+    path('box/use', views.AdminButtonBoxUseListCreateView.as_view(), name='admin_box_use'),
+]
diff --git a/django/api_admin/views.py b/django/api_admin/views.py
new file mode 100644
index 0000000..cc8d154
--- /dev/null
+++ b/django/api_admin/views.py
@@ -0,0 +1,76 @@
+"""
+    File for setting the view classes used from the api_admin django app.
+"""
+
+
+from shared_models.models import ButtonBox, ButtonBoxUse
+from shared_models.serializers import ButtonBoxSerializer, ButtonBoxUseSerializer
+from rest_framework import generics, status
+from rest_framework.response import Response
+from rest_framework.permissions import IsAuthenticated
+
+from backend.settings import DEFAULT_DJANGO_DEBUG
+
+
+class AdminButtonBoxListCreateDeleteView(generics.ListCreateAPIView):
+    """Class for setting the API views for Create, Read and Delete the entries at the ButtonBox model.
+    """
+
+    permission_classes = [IsAuthenticated]
+
+    queryset = ButtonBox.objects.all()
+    serializer_class = ButtonBoxSerializer
+
+    def delete(self, request, *args, **kwargs):
+        """Method for deleting all ButtonBox model objects.
+        """
+
+        # Delete only by debug mode
+        if DEFAULT_DJANGO_DEBUG:
+            ButtonBox.objects.all().delete()
+            return Response(status=status.HTTP_204_NO_CONTENT)
+        
+        else:
+            ret_dict = {
+                "error": False,
+                "message": "You can only delete all in debub mode --> set the 'DEFAULT_DJANGO_DEBUG' to 'True'"
+            }
+            return Response(ret_dict, status=status.HTTP_412_PRECONDITION_FAILED)
+
+
+class AdminButtonBoxRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView):
+    """Class for setting the API views for Updating entries at the ButtonBox model.
+    """
+
+    permission_classes = [IsAuthenticated]
+
+    queryset = ButtonBox.objects.all()
+    serializer_class = ButtonBoxSerializer
+    ### pk --> primary_key
+    lookup_field = "pk"
+
+
+class AdminButtonBoxUseListCreateView(generics.ListCreateAPIView):
+    """Class for setting the API views for Creating entries at the ButtonBoxUse model.
+    """
+
+    permission_classes = [IsAuthenticated]
+
+    queryset = ButtonBoxUse.objects.all()
+    serializer_class = ButtonBoxUseSerializer
+
+    def delete(self, request, *args, **kwargs):
+        """Method for deleting all ButtonBoxUse model objects.
+        """
+
+        # Delete only by debug mode
+        if DEFAULT_DJANGO_DEBUG:
+            ButtonBoxUse.objects.all().delete()
+            return Response(status=status.HTTP_204_NO_CONTENT)
+        
+        else:
+            ret_dict = {
+                "error": False,
+                "message": "You can only delete all in debub mode --> set the 'DEFAULT_DJANGO_DEBUG' to 'True'"
+            }
+            return Response(ret_dict, status=status.HTTP_412_PRECONDITION_FAILED)
diff --git a/django/api_auth/__init__.py b/django/api_auth/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django/api_auth/apps.py b/django/api_auth/apps.py
new file mode 100644
index 0000000..0d4e91b
--- /dev/null
+++ b/django/api_auth/apps.py
@@ -0,0 +1,12 @@
+"""
+    Default setted file from django for an added app.
+"""
+
+from django.apps import AppConfig
+
+
+class ApiAuthConfig(AppConfig):
+    """Default setted SharedModelsConfig class for the app.
+    """
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "api_auth"
diff --git a/django/api_auth/templates/login.html b/django/api_auth/templates/login.html
new file mode 100644
index 0000000..f798093
--- /dev/null
+++ b/django/api_auth/templates/login.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <title>Login</title>
+</head>
+
+<body>
+    <h2>Login</h2>
+    {% if user.is_authenticated %}
+        <p>Logged in as {{ user.username }}</p>
+        <a href="{% url 'logout' %}">Logout</a>
+    {% else %}
+        <p>You are not logged in.</p>
+    {% endif %}
+    <form method="post">
+        {% csrf_token %}
+        {{ form.as_p }}
+        <button type="submit">Login</button>
+    </form>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/django/api_auth/templates/logout.html b/django/api_auth/templates/logout.html
new file mode 100644
index 0000000..71a9b56
--- /dev/null
+++ b/django/api_auth/templates/logout.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <title>Logout</title>
+</head>
+
+<body>
+    <h2>You have been logged out.</h2>
+    <a href="{% url 'login' %}">Log in again</a>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/django/api_auth/urls.py b/django/api_auth/urls.py
new file mode 100644
index 0000000..a7872fb
--- /dev/null
+++ b/django/api_auth/urls.py
@@ -0,0 +1,22 @@
+"""
+    File for setting the URLs used from the api_auth django app.
+"""
+
+### TOKEN
+from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
+
+### LOGIN
+from django.contrib.auth import views as auth_views
+
+from django.urls import path
+from . import views
+
+
+urlpatterns = [
+    path('test/open', views.TestOpenView.as_view(), name='test-open'),
+    path('test/closed', views.TestClosedView.as_view(), name='test-closed'),
+    path('login/', auth_views.LoginView.as_view(template_name='login.html'), name='login'),
+    path('logout/', views.custom_logout, name='logout'),
+    path("token/", TokenObtainPairView.as_view(), name='token_obtain_pair'),
+    path("token/refresh/", TokenRefreshView.as_view(), name='token_refresh'),
+]
diff --git a/django/api_auth/views.py b/django/api_auth/views.py
new file mode 100644
index 0000000..1946c4f
--- /dev/null
+++ b/django/api_auth/views.py
@@ -0,0 +1,97 @@
+
+"""
+TODO: Add this !!!
+"""
+from rest_framework import status
+from rest_framework.views import APIView
+from rest_framework import permissions
+# from rest_framework.permissions import IsAuthenticated
+from rest_framework.renderers import JSONRenderer
+from rest_framework_simplejwt.authentication import JWTAuthentication
+from django.views.decorators.http import require_GET
+from django.contrib.auth import logout
+from django.shortcuts import redirect
+from django.http import JsonResponse
+
+
+class TestOpenView(APIView):
+    """Class for testing an end-point without authentication.
+    """
+
+    permission_classes = [permissions.AllowAny]
+
+    def get(self, request=None, format=None):
+        """Method for dealing with the GET requests done at this view.
+
+        Parameters
+        ----------
+        request: rest_framework.request.Request
+            The parameters from the GET request.
+        format: None
+            If some format was sent.
+
+        Returns
+        -------
+        rest_framework.response.Response
+            The response object.
+        """
+
+        ret_dict = {
+            "error": False,
+            "message": "This is the auth test open end-point"
+        }
+
+        return JsonResponse(ret_dict, status=status.HTTP_200_OK)
+
+
+class TestClosedView(APIView):
+    """Class for testing the authentication.
+    """
+
+    # permission_classes = [IsAuthenticated]
+    authentication_classes = [JWTAuthentication]
+    renderer_classes = [JSONRenderer]
+
+    def get(self, request=None, format=None):
+        """Method for dealing with the GET requests done at this view.
+
+        Parameters
+        ----------
+        request: rest_framework.request.Request
+            The parameters from the GET request.
+        format: None
+            If some format was sent.
+
+        Returns
+        -------
+        rest_framework.response.Response
+            The response object.
+        """
+
+        ret_dict = {
+            "error": False,
+            "message": "You are authenticated"
+        }
+
+        return JsonResponse(ret_dict, status=status.HTTP_200_OK)
+
+
+# from rest_framework.response import Response
+@require_GET
+def custom_logout(request):
+    """Method for logging out from django.
+
+    Parameters
+    ----------
+    request: rest_framework.request.Request
+        The parameters from the GET request.
+
+    Returns
+    -------
+    redirect:
+        Redirects to the login page.
+    """
+
+    logout(request)
+    # return HttpResponse("You have been logged out.")  # Temporary for debugging
+    return redirect('/auth/login/')
diff --git a/django/backend/__init__.py b/django/backend/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django/backend/asgi.py b/django/backend/asgi.py
new file mode 100644
index 0000000..20e4b44
--- /dev/null
+++ b/django/backend/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for backend project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
+
+application = get_asgi_application()
diff --git a/django/backend/settings.py b/django/backend/settings.py
new file mode 100644
index 0000000..146db36
--- /dev/null
+++ b/django/backend/settings.py
@@ -0,0 +1,212 @@
+"""
+Django settings for myproject project.
+
+Generated by 'django-admin startproject' using Django 5.1.6.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.1/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/5.1/ref/settings/
+"""
+import os
+from pathlib import Path
+from datetime import timedelta
+from dotenv import load_dotenv
+
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = "django-insecure-9^f+%n36vxg-^q-iu1$x4&+--&u+i4ds@!xhe@+i8w(-wg00bu"
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+# ALLOWED_HOSTS = []
+ALLOWED_HOSTS = ['*']
+
+###
+### LOAD VARIABLES
+###
+
+# Try loading from the .env file, if it exists...
+load_dotenv()
+
+try:
+    DEFAULT_TOKEN_EXPIRE_MIN = int(os.getenv("DEFAULT_TOKEN_EXPIRE_MIN"))
+
+except Exception:
+    DEFAULT_TOKEN_EXPIRE_MIN = 10080
+
+try:
+    DEFAULT_TOKEN_REFRESH_DAYS = int(os.getenv("DEFAULT_TOKEN_REFRESH_DAYS"))
+
+except Exception:
+    DEFAULT_TOKEN_REFRESH_DAYS = 7
+
+try:
+    fetched = int(os.getenv("DEFAULT_DJANGO_DEBUG"))
+    if fetched in ('YES', 'yes', 'true', 'True', 'TRUE'):
+        DEFAULT_DJANGO_DEBUG = True
+    else:
+        DEFAULT_DJANGO_DEBUG = False
+
+except Exception:
+    DEFAULT_DJANGO_DEBUG = False
+
+print("\n---> DJANGO VARIABLES")
+print(f"DEFAULT_TOKEN_EXPIRE_MIN: {DEFAULT_TOKEN_EXPIRE_MIN}")
+print(f"DEFAULT_TOKEN_REFRESH_DAYS: {DEFAULT_TOKEN_REFRESH_DAYS}")
+print(f"DEFAULT_DJANGO_DEBUG: {DEFAULT_DJANGO_DEBUG}")
+print("<---DJANGO VARIABLES\n")
+
+# Application definition
+INSTALLED_APPS = [
+    "django.contrib.admin",
+    "django.contrib.auth",
+    "django.contrib.contenttypes",
+    "django.contrib.sessions",
+    "django.contrib.messages",
+    "django.contrib.staticfiles",
+    ### ADDED
+    "rest_framework",
+    ### SWAGGER
+    "drf_yasg",
+    ### JWT TOKEN
+    "rest_framework_simplejwt",
+    ### SSL
+    "sslserver",
+    ### MY STUFF
+    "shared_models",
+    "api",
+    "api_auth",
+    "api_admin",
+]
+
+###
+### AUTH
+###
+REST_FRAMEWORK = {
+    'DEFAULT_PERMISSION_CLASSES': [
+        'rest_framework.permissions.IsAuthenticated',  # Require authentication for all endpoints
+    ],
+    'DEFAULT_AUTHENTICATION_CLASSES': (
+        'rest_framework.authentication.SessionAuthentication',
+        'rest_framework.authentication.BasicAuthentication',
+    ),
+}
+SESSION_ENGINE = "django.contrib.sessions.backends.db"
+LOGIN_REDIRECT_URL = '/swagger/'  # Redirect here after login
+LOGIN_URL = '/auth/login/'  # The URL to redirect to for login
+LOGOUT_REDIRECT_URL = '/auth/login/'  # Redirect here after logout
+###
+### TOKEN
+###
+SIMPLE_JWT = {
+    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=DEFAULT_TOKEN_EXPIRE_MIN),
+    'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=DEFAULT_TOKEN_REFRESH_DAYS),
+    'SLIDING_TOKEN_LIFETIME': timedelta(days=30),
+    'SLIDING_TOKEN_REFRESH_LIFETIME_LATE_USER': timedelta(days=3),
+    'SLIDING_TOKEN_LIFETIME_LATE_USER': timedelta(days=30),
+}
+
+
+MIDDLEWARE = [
+    "django.middleware.security.SecurityMiddleware",
+    "django.contrib.sessions.middleware.SessionMiddleware",
+    "django.middleware.common.CommonMiddleware",
+    "django.middleware.csrf.CsrfViewMiddleware",
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
+    "django.contrib.messages.middleware.MessageMiddleware",
+    "django.middleware.clickjacking.XFrameOptionsMiddleware",
+    ###
+    ### ADDED
+    ###
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.middleware.security.SecurityMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = "backend.urls"
+
+TEMPLATES = [
+    {
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "DIRS": [],
+        "APP_DIRS": True,
+        "OPTIONS": {
+            "context_processors": [
+                "django.template.context_processors.debug",
+                "django.template.context_processors.request",
+                "django.contrib.auth.context_processors.auth",
+                "django.contrib.messages.context_processors.messages",
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = "backend.wsgi.application"
+
+
+# Database
+# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
+
+DATABASES = {
+    "default": {
+        "ENGINE": "django.db.backends.sqlite3",
+        "NAME": BASE_DIR / "db.sqlite3",
+    }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+    },
+    {
+        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+    },
+    {
+        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+    },
+    {
+        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+    },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/5.1/topics/i18n/
+
+LANGUAGE_CODE = "en-us"
+
+# TIME_ZONE = "UTC"
+TIME_ZONE = 'Europe/Berlin'
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/5.1/howto/static-files/
+
+STATIC_URL = "static/"
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
diff --git a/django/backend/urls.py b/django/backend/urls.py
new file mode 100644
index 0000000..4b69704
--- /dev/null
+++ b/django/backend/urls.py
@@ -0,0 +1,55 @@
+"""
+URL configuration for backend project.
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+    https://docs.djangoproject.com/en/5.1/topics/http/urls/
+Examples:
+Function views
+    1. Add an import:  from my_app import views
+    2. Add a URL to urlpatterns:  path('', views.home, name='home')
+Class-based views
+    1. Add an import:  from other_app.views import Home
+    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
+Including another URLconf
+    1. Import the include() function: from django.urls import include, path
+    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
+"""
+
+from rest_framework import permissions
+from drf_yasg.views import get_schema_view
+from drf_yasg import openapi
+from django.contrib import admin
+from django.urls import path, include
+
+
+schema_view = get_schema_view(
+    openapi.Info(
+        title="c3buttons",
+        default_version='1.0.0',
+        description="Swagger Documentation for c3buttons",
+        # terms_of_service="https://link.to.something",
+        contact=openapi.Contact(name="fejao", email="dont@bug.me"),
+        license=openapi.License(name="MIT"),
+    ),
+    ### AUTH
+    public=False,
+    permission_classes=(permissions.IsAuthenticated,),
+
+)
+
+
+urlpatterns = [
+    path("admin/", admin.site.urls),
+    ###
+    ### SWAGGER
+    ###
+    path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
+    path('swagger.json/', schema_view.without_ui(cache_timeout=0), name='schema-json'),
+    ###
+    ### APPS
+    ###
+    path('auth/', include('api_auth.urls')),
+    # path("api/box/", include("api.urls")),
+    path("api/", include("api.urls")),
+    path("api-admin/", include("api_admin.urls")),
+]
diff --git a/django/backend/wsgi.py b/django/backend/wsgi.py
new file mode 100644
index 0000000..ae9503c
--- /dev/null
+++ b/django/backend/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for backend project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
+
+application = get_wsgi_application()
diff --git a/django/manage.py b/django/manage.py
new file mode 100755
index 0000000..1917e46
--- /dev/null
+++ b/django/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+    """Run administrative tasks."""
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
+    try:
+        from django.core.management import execute_from_command_line
+    except ImportError as exc:
+        raise ImportError(
+            "Couldn't import Django. Are you sure it's installed and "
+            "available on your PYTHONPATH environment variable? Did you "
+            "forget to activate a virtual environment?"
+        ) from exc
+    execute_from_command_line(sys.argv)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/django/requirements.txt b/django/requirements.txt
new file mode 100644
index 0000000..54dae68
--- /dev/null
+++ b/django/requirements.txt
@@ -0,0 +1,10 @@
+dotenv
+### DJANGO
+django
+djangorestframework
+### SWAGGER
+drf-yasg
+### TOKEN
+djangorestframework-simplejwt
+### SSL
+django-sslserver
diff --git a/django/shared_models/__init__.py b/django/shared_models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django/shared_models/apps.py b/django/shared_models/apps.py
new file mode 100644
index 0000000..6668dfb
--- /dev/null
+++ b/django/shared_models/apps.py
@@ -0,0 +1,12 @@
+"""
+    Default setted file from django for an added app.
+"""
+
+from django.apps import AppConfig
+
+
+class SharedModelsConfig(AppConfig):
+    """Default setted SharedModelsConfig class for the app.
+    """
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "shared_models"
diff --git a/django/shared_models/migrations/0001_initial.py b/django/shared_models/migrations/0001_initial.py
new file mode 100644
index 0000000..57ae4ce
--- /dev/null
+++ b/django/shared_models/migrations/0001_initial.py
@@ -0,0 +1,38 @@
+# Generated by Django 5.1.6 on 2025-03-03 18:22
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ButtonBox',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=40, unique=True)),
+                ('pin_button', models.PositiveIntegerField(unique=True)),
+                ('pin_led', models.PositiveIntegerField(unique=True)),
+            ],
+            options={
+                'db_table': 'button_box',
+            },
+        ),
+        migrations.CreateModel(
+            name='ButtonBoxUse',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('date', models.DateTimeField(auto_now_add=True)),
+                ('box_used', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shared_models.buttonbox')),
+            ],
+            options={
+                'db_table': 'button_box_use',
+            },
+        ),
+    ]
diff --git a/django/shared_models/migrations/__init__.py b/django/shared_models/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django/shared_models/models.py b/django/shared_models/models.py
new file mode 100644
index 0000000..f7010ef
--- /dev/null
+++ b/django/shared_models/models.py
@@ -0,0 +1,49 @@
+"""
+    Serializers for the models shared among this django.
+"""
+
+from django.db import models
+
+
+class ButtonBox(models.Model):
+    """The model for storing the information about the button box.
+    """
+    name = models.CharField(max_length=40, unique=True)
+    # pin_number = models.PositiveIntegerField(unique=True)
+    pin_button = models.PositiveIntegerField(unique=True)
+    pin_led = models.PositiveIntegerField(unique=True)
+
+    class Meta:
+        """ Meta class for the Button Box model.
+        Used for setting the name ot the table at the DB.
+        """
+        db_table = "button_box"
+
+    def __str__(self):
+        """Override from the default __str__ method for returning the name of the box instead of it's ID.
+        """
+        return str(self.name)
+
+
+class ButtonBoxUse(models.Model):
+    """The model for storing the information about the button box uses.
+    """
+
+    box_used = models.ForeignKey(
+        ButtonBox,
+        on_delete=models.CASCADE,
+    )
+    date = models.DateTimeField(auto_now_add=True, blank=True)
+
+    class Meta:
+        """ Meta class for the Button Box Use model.
+        Used for setting the name ot the table at the DB.
+        """
+        db_table = "button_box_use"
+
+    @property
+    def box_name(self):
+        """Property method for fetching the name of the box for the box used.
+        """
+        box_obj = ButtonBox.objects.get(name=self.box_used)
+        return box_obj.name
diff --git a/django/shared_models/serializers.py b/django/shared_models/serializers.py
new file mode 100644
index 0000000..4bc1c3b
--- /dev/null
+++ b/django/shared_models/serializers.py
@@ -0,0 +1,41 @@
+"""
+    Serializer used for the used shared models among the apps.
+"""
+
+from rest_framework import serializers
+from .models import ButtonBox, ButtonBoxUse
+
+
+class ButtonBoxSerializer(serializers.ModelSerializer):
+    """The serializer for the information about the button box.
+    """
+
+    class Meta:
+        """ Meta class for the ButtonBoxSerializer.
+        Used for returning only the information needed for the ButtonBox model.
+        """
+        model = ButtonBox
+        fields = [
+            # "id",
+            "name",
+            # "pin_number",
+            "pin_button",
+            "pin_led",
+        ]
+
+
+class ButtonBoxUseSerializer(serializers.ModelSerializer):
+    """The serializer for the information about the button box uses.
+    """
+
+    class Meta:
+        """ Meta class for the ButtonBoxSerializer.
+        Used for returning only the information needed for the ButtonBoxUse model.
+        """
+        model = ButtonBoxUse
+        fields = [
+            # "id",
+            "box_used",
+            "box_name",
+            "date",
+        ]
-- 
GitLab


From 748967d861ff9c357c4d236069e395c79d0b99b6 Mon Sep 17 00:00:00 2001
From: fejao <mail@fejao.de>
Date: Wed, 5 Mar 2025 17:19:11 +0100
Subject: [PATCH 4/6] Added docker

---
 docker/README.md          |   0
 docker/django/Dockerfile  |  30 +++++++++++
 docker/docker-compose.yml | 108 ++++++++++++++++++++++++++++++++++++++
 docker/gpio/Dockerfile    |  50 ++++++++++++++++++
 4 files changed, 188 insertions(+)
 create mode 100644 docker/README.md
 create mode 100644 docker/django/Dockerfile
 create mode 100644 docker/docker-compose.yml
 create mode 100644 docker/gpio/Dockerfile

diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/docker/django/Dockerfile b/docker/django/Dockerfile
new file mode 100644
index 0000000..646c058
--- /dev/null
+++ b/docker/django/Dockerfile
@@ -0,0 +1,30 @@
+FROM python:3.10.15-bullseye
+
+# Set variables
+ENV DEBIAN_FRONTEND=noninteractive
+ENV TZ="Europe/Berlin"
+
+# Install dependencies
+RUN apt-get update -qq  && apt-get upgrade -qqy \
+    && apt-get install -qqy \
+    python3-pip \
+    htop \
+    vim \
+    && apt-get clean \
+    && rm -rf /var/lib/apt/lists/*
+
+# Create the folder at the container
+WORKDIR /c3buttons
+
+# Install python requirements
+COPY ../../django/requirements.txt ./
+RUN pip3 install -r requirements.txt
+
+# Copy App
+COPY ../../django/ .
+
+### TEST
+# CMD ["sleep", "10000000"]
+
+### PROD
+CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"]
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 0000000..dd06342
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,108 @@
+---
+
+services:
+  django:
+    image: c3buttons-django:latest
+    container_name: c3buttons-django
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD-SHELL", "curl -f http://localhost:8000/auth/test/open || exit 1"]
+      interval: 30s
+      timeout: 5s
+      retries: 5
+    ### DISABLE PORTS FOR PRODUCTION
+    ports:
+      - '8000:8000'
+    # volumes:
+    #   - ./django/data/db.sqlite3:/c3buttons/db.sqlite3
+    environment:
+      ###
+      ### PLEASE CHANGE THIS
+      ###
+      - DEFAULT_TOKEN_REFRESH_DAYS=7
+      - DEFAULT_TOKEN_EXPIRE_MIN=10080 # 10080 min --> 7 days
+      - DEFAULT_DJANGO_DEBUG=true
+
+  gpio:
+    image: c3buttons-gpio:latest
+    container_name: c3buttons-gpio
+    restart: unless-stopped
+    #########
+    # healthcheck:
+    #   test: ["CMD-SHELL", "curl -f http://localhost:8000/auth/test/open || exit 1"]
+    #   interval: 30s
+    #   timeout: 5s
+    #   retries: 5
+    #########
+    # depends_on:
+    #   django:
+    #     condition: service_healthy
+    links:
+      - django
+    privileged: true
+    devices:
+      - /dev/gpiomem:/dev/gpiomem
+    volumes:
+      # - /dev/gpiomem:/dev/gpiomem
+      ### DEBUG
+      - ../gpio_buttons:/c3buttons
+    environment:
+      ###
+      ### PLEASE CHANGE THIS
+      ###
+      - 'DATABASE_API_TOKEN=<PLEASE_CHANGE_THIS>'
+      - 'BOXES_USED="BUTTON_BOX_1, BUTTON_BOX_2, BUTTON_BOX_3"'
+      # - 'BOXES_USED="BUTTON_BOX_1, BUTTON_BOX_2"'
+      - 'BUTTON_BOX_1={"pin_button": 16, "pin_led": 14}'
+      - 'BUTTON_BOX_2={"pin_button": 20, "pin_led": 19}'
+      - 'BUTTON_BOX_3={"pin_button": 21, "pin_led": 13}'
+
+  grafana:
+    image: grafana/grafana:latest
+    container_name: c3buttons-grafana
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"]
+      interval: 30s
+      timeout: 5s
+      retries: 5
+    # depends_on:
+    #   django:
+    #     condition: service_healthy
+    links:
+      - django
+    ### DISABLE PORTS FOR PRODUCTION
+    ports:
+      - '3000:3000'
+    volumes:
+      - ./grafana/data:/var/lib/grafana
+    environment:
+      - FOO=BAR
+      - GF_FEATURE_TOGGLES_PUBLICDASHBOARDS=true
+      - TLS_SKIP_VERIFY_INSECURE=true
+
+  proxy:
+    image: 'jc21/nginx-proxy-manager:latest'
+    container_name: c3buttons-proxy
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD-SHELL", "curl -f http://localhost:81 || exit 1"]
+      interval: 30s
+      timeout: 5s
+      retries: 5
+    # depends_on:
+    #   django:
+    #     condition: service_healthy
+    #   grafana:
+    #     condition: service_healthy
+    links:
+      - django
+      - grafana
+    ports:
+      - '80:80'
+      - '443:443'
+      ### This is the admin port, disable for Production
+      - '81:81'
+    volumes:
+      - ./proxy/data:/data
+      - ./proxy/letsencrypt:/etc/letsencrypt
diff --git a/docker/gpio/Dockerfile b/docker/gpio/Dockerfile
new file mode 100644
index 0000000..d1771ad
--- /dev/null
+++ b/docker/gpio/Dockerfile
@@ -0,0 +1,50 @@
+FROM ubuntu:24.04
+
+# Set variables
+ENV DEBIAN_FRONTEND=noninteractive
+ENV TZ="Europe/Berlin"
+
+# Install dependencies
+RUN apt-get update -qq  && apt-get upgrade -qqy \
+# python3-dev \
+# python3-venv \
+    && apt-get install -qqy \
+    python3-pip \
+    python3-lgpio \
+    python3-pigpio \
+    python3-rpi.gpio \
+    htop \
+    vim \
+    && apt-get clean \
+    && rm -rf /var/lib/apt/lists/*
+
+# New debian base FUCKING shit
+RUN rm -rfv /usr/lib/python3*/EXTERNALLY-MANAGED
+
+# Change user for installing python packages -> Set user and group
+ARG user=appuser
+ARG group=appuser
+ARG uid=2000
+ARG gid=2000
+RUN groupadd -g ${gid} ${group}
+# <--- the '-m' create a user home directory
+RUN useradd -u ${uid} -g ${group} -s /bin/sh -m ${user}
+
+# Create the folder at the container
+WORKDIR /home/appuser/c3buttons
+
+# Install python requirements
+COPY ../../gpio_buttons/requirements.txt ./
+RUN pip3 install -r requirements.txt
+
+# Install GPIO
+RUN pip3 install gpiozero --break-system-packages
+
+# Copy App
+COPY ../../gpio_buttons/ .
+
+### TEST
+CMD ["sleep", "1000000"]
+
+### PROD
+# CMD ["python3", "main.py"]
-- 
GitLab


From 5e29c0534645c709bee582ee301b62053a8b8724 Mon Sep 17 00:00:00 2001
From: fejao <mail@fejao.de>
Date: Wed, 5 Mar 2025 17:20:47 +0100
Subject: [PATCH 5/6] Added gpio_buttons

---
 gpio_buttons/.env                             |  52 ++++
 gpio_buttons/logs/example.log                 |   0
 gpio_buttons/main.py                          |  92 +++++++
 gpio_buttons/requirements.txt                 |   7 +
 gpio_buttons/utils/boxes/__init__.py          |   0
 gpio_buttons/utils/boxes/app_boxes.py         |  98 +++++++
 gpio_buttons/utils/boxes/box_button.py        |  70 +++++
 gpio_buttons/utils/boxes/box_led.py           |  72 +++++
 gpio_buttons/utils/boxes/box_pin.py           |  42 +++
 gpio_buttons/utils/configuration/__init__.py  |   0
 .../utils/configuration/app_configuration.py  | 178 +++++++++++++
 .../utils/configuration/app_logger.py         | 159 +++++++++++
 gpio_buttons/utils/configuration/config.yml   |  27 ++
 .../utils/configuration/default_values.py     |  57 ++++
 gpio_buttons/utils/configuration/from_file.py |  32 +++
 .../configuration/from_file_environment.py    | 250 ++++++++++++++++++
 .../utils/configuration/from_file_yaml.py     |  83 ++++++
 gpio_buttons/utils/database/__init__.py       |   0
 gpio_buttons/utils/database/app_database.py   |  64 +++++
 gpio_buttons/utils/database/from_api.py       | 110 ++++++++
 gpio_buttons/utils/database/from_database.py  |  31 +++
 .../utils/database/from_local_file.py         | 159 +++++++++++
 pycodestyle.cfg                               |   8 +
 23 files changed, 1591 insertions(+)
 create mode 100644 gpio_buttons/.env
 create mode 100644 gpio_buttons/logs/example.log
 create mode 100755 gpio_buttons/main.py
 create mode 100644 gpio_buttons/requirements.txt
 create mode 100644 gpio_buttons/utils/boxes/__init__.py
 create mode 100644 gpio_buttons/utils/boxes/app_boxes.py
 create mode 100644 gpio_buttons/utils/boxes/box_button.py
 create mode 100644 gpio_buttons/utils/boxes/box_led.py
 create mode 100644 gpio_buttons/utils/boxes/box_pin.py
 create mode 100644 gpio_buttons/utils/configuration/__init__.py
 create mode 100644 gpio_buttons/utils/configuration/app_configuration.py
 create mode 100644 gpio_buttons/utils/configuration/app_logger.py
 create mode 100644 gpio_buttons/utils/configuration/config.yml
 create mode 100644 gpio_buttons/utils/configuration/default_values.py
 create mode 100644 gpio_buttons/utils/configuration/from_file.py
 create mode 100644 gpio_buttons/utils/configuration/from_file_environment.py
 create mode 100644 gpio_buttons/utils/configuration/from_file_yaml.py
 create mode 100644 gpio_buttons/utils/database/__init__.py
 create mode 100644 gpio_buttons/utils/database/app_database.py
 create mode 100644 gpio_buttons/utils/database/from_api.py
 create mode 100644 gpio_buttons/utils/database/from_database.py
 create mode 100644 gpio_buttons/utils/database/from_local_file.py
 create mode 100644 pycodestyle.cfg

diff --git a/gpio_buttons/.env b/gpio_buttons/.env
new file mode 100644
index 0000000..f1c91a9
--- /dev/null
+++ b/gpio_buttons/.env
@@ -0,0 +1,52 @@
+
+DESCRIPTION='<PLEASE_SET_THIS>'
+
+# DEBUG='false'
+DEBUG='true'
+VERBOSE='false'
+
+###
+### DATABASE_TYPE --> 'local_file' = using local running django with SQLite | 'api' = access it with API
+###
+# DATABASE_TYPE='local_file'
+DATABASE_TYPE='api'
+
+###
+### DATABASE_TYPE: local_file
+###
+# Pi
+DATABASE_LOCAL_DB_FILE_PATH='/home/pi/Coding/c3buttons/django/db.sqlite3'
+# # Local
+# DATABASE_LOCAL_DB_FILE_PATH='/home/fejao/Coding/CCC/git_cccv/c3buttons/django/db.sqlite3'
+
+###
+### DATABASE_TYPE: api
+###
+# Pi
+DATABASE_API_URL='http://django:8000'
+DATABASE_API_TOKEN='<PLEASE_SER_THIS>'
+# # Local
+# DATABASE_API_URL='http://localhost:8000'
+# DATABASE_API_TOKEN='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzQxNjMxMTI3LCJpYXQiOjE3NDEwMjYzMjcsImp0aSI6Ijk5MzFjMjhkNGE2MDQ4MTg5ZGYwYTEzZDcyNzE2NDI5IiwidXNlcl9pZCI6MX0.fNIm-6bai4jJkL5NMSW1K9mWIP_XmHzPEkTxgzqJd_4'
+
+###
+### BOXES CONFIG
+###
+BOX_LED_BLINK_TIMES=3
+
+# BOX_LED_ENABLED='false'
+BOX_LED_ENABLED='true'
+
+# BOX_LED_AWAYS_ON='true'
+BOX_LED_AWAYS_ON='false'
+
+### BOX_BUTTON_BOUNCE_TIME in microseconds
+BOX_BUTTON_BOUNCE_TIME= '0.1'
+
+###
+### BOXES USED
+###
+BUTTON_BOX_1='{"pin_button": 16, "pin_led": 14}'
+BUTTON_BOX_2='{"pin_button": 20, "pin_led": 19}'
+# This is the list of the boxes used, if you add new ones, please alse add it's configuration
+BOXES_USED='BUTTON_BOX_1, BUTTON_BOX_2'
diff --git a/gpio_buttons/logs/example.log b/gpio_buttons/logs/example.log
new file mode 100644
index 0000000..e69de29
diff --git a/gpio_buttons/main.py b/gpio_buttons/main.py
new file mode 100755
index 0000000..426b199
--- /dev/null
+++ b/gpio_buttons/main.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+
+import sys
+###
+from signal import pause
+
+try:
+    from utils.configuration.app_configuration import AppConfiguration
+except ImportError as e:
+    print("Unable to import 'AppConfiguration' from 'utils.configuration.app_configuration.py' file at the 'main.py'")
+    sys.exit(1)
+
+try:
+    from utils.database.app_database import AppDatabase
+except ImportError as e:
+    print("Unable to import 'AppDatabase' from 'utils.database.app_database.py' file at the 'main.py'")
+    sys.exit(1)
+
+try:
+    from utils.boxes.app_boxes import AppBoxes
+except ImportError as e:
+    print("Unable to import 'AppDatabase' from 'utils.database.app_database.py' file at the 'main.py'")
+    sys.exit(1)
+
+
+#############
+#############
+#############
+
+
+BTN_COUNTER = 0
+
+def btn_pressed():
+    global BTN_COUNTER
+    print(f"button pressed | counter: {BTN_COUNTER}")
+    BTN_COUNTER += 1
+
+def btn_pressed_2():
+    global BTN_COUNTER
+    print(f"button pressed_2 | counter: {BTN_COUNTER}")
+    BTN_COUNTER += 1
+
+# main function
+def main():
+    print("\n-------> START \n")
+
+    # Get the configuration, from file or parsed...
+    configuration = AppConfiguration()
+
+    ###
+    ### DEBUG
+    ###
+    from pprint import pp
+    # import pdb; pdb.set_trace()
+    # pp(configuration.dict_values)
+
+    # Set the used DB from the configuration...
+    database = AppDatabase(configuration)
+
+    ###
+    ### LOCALSQLite
+    ###
+    # ### WORKS
+    # foo = database.db.table_select_all(configuration.database_table_name_button_box)
+    # import pdb; pdb.set_trace()
+    # pp(foo)
+
+    ###
+    ### API
+    ###
+    # # import pdb; pdb.set_trace()
+    # test_data = {
+    #     # "id": "1"
+    #     "id": "2"
+    # }
+    # foo = database.db.post(endpoint="api/box/use", json=test_data)
+    # pp(foo)
+
+    # bar = database.db.get(endpoint="api/box/use/count")
+    # pp(bar)
+
+    # Set the Button Boxes
+    boxes = AppBoxes(configuration, database)
+
+    import pdb; pdb.set_trace()
+    pp(boxes)
+
+    print("\n <------- FINISH \n")
+
+
+if __name__=="__main__":
+    main()
diff --git a/gpio_buttons/requirements.txt b/gpio_buttons/requirements.txt
new file mode 100644
index 0000000..51d4fb6
--- /dev/null
+++ b/gpio_buttons/requirements.txt
@@ -0,0 +1,7 @@
+# argparse
+###
+# logging
+pyaml-env
+dotenv
+requests
+# gpiozero
diff --git a/gpio_buttons/utils/boxes/__init__.py b/gpio_buttons/utils/boxes/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gpio_buttons/utils/boxes/app_boxes.py b/gpio_buttons/utils/boxes/app_boxes.py
new file mode 100644
index 0000000..73244cb
--- /dev/null
+++ b/gpio_buttons/utils/boxes/app_boxes.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+"""
+   utils.boxes.app_boxes.py file for the c3buttons system.
+"""
+
+###
+### DEPENDENCIES
+###
+import sys
+from signal import pause
+
+### LOCAL
+try:
+    from utils.configuration.app_logger import set_logger_from_config
+except ImportError as e:
+    print("Unable to import 'setup_logger' from 'utils.configuration.app_logger.py' file at 'utils.database.app_database.py' file")
+    sys.exit(1)
+
+try:
+    from .box_led import BoxLed
+except ImportError as e:
+    print("Unable to import 'BoxLed' from 'utils.boxes.box_led.py' file at 'utils.boxes.app_boxes.py' file" )
+    sys.exit(1)
+
+try:
+    from .box_button import BoxButton
+except ImportError as e:
+    print("Unable to import 'BoxButton' from 'utils.boxes.box_button.py' file at 'utils.boxes.app_boxes.py' file" )
+    sys.exit(1)
+
+
+class AppBoxes():
+    """
+        Class for setting the Boxes of the app.
+    """
+
+    def __init__(self, configuration: object, database: object) -> None:
+        """
+            Function for initialiazing the AppConfiguration class.
+            Sets the class variables.
+
+            Returns
+            -------
+            None
+        """
+
+        self.configuration = configuration
+        self.database = database
+
+        self.logger = set_logger_from_config(name=__name__, config=self.configuration)
+
+        self.buttons = {}
+        self.set_boxes()
+        self.set_button_functions()
+
+    def set_boxes(self):
+
+        self.logger.debug("set_boxes")
+
+        for box_name in self.configuration.boxes_used:
+            if self.configuration.boxes.get(box_name) is not None:
+                box_info = self.configuration.boxes.get(box_name)
+                if box_info.get('pin_button') is None:
+                    print(f"\n\n The 'pin_button' value for box with name '{box_name}' was not set" )
+                    sys.exit(1)
+
+                if box_info.get('pin_led') is None:
+                    print(f"\n\n The 'pin_led' value for box with name '{box_name}' was not set" )
+                    sys.exit(1)
+
+                box_led = None
+                if self.configuration.box_led_enabled:
+                    box_led = BoxLed(
+                        configuration=self.configuration,
+                        box_name=box_name,
+                    )
+
+                box_button = BoxButton(
+                    configuration=self.configuration,
+                    box_name=box_name,
+                    database=self.database,
+                    box_led=box_led,
+                )
+
+                self.buttons[box_name] = box_button
+
+    def set_button_functions(self):
+
+        self.logger.debug("set_button_functions")
+
+        try:
+            for btn_name, btn in self.buttons.items():
+                btn.button.when_pressed = btn.activated
+                self.logger.debug(f"Button functions for box: '{btn_name}' activated...")
+
+            pause()
+        finally:
+            pass
diff --git a/gpio_buttons/utils/boxes/box_button.py b/gpio_buttons/utils/boxes/box_button.py
new file mode 100644
index 0000000..8a9d3ff
--- /dev/null
+++ b/gpio_buttons/utils/boxes/box_button.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+"""
+   utils.database.from_api.py file for the c3 buttons.
+"""
+
+
+###
+### DEPENDENCIES
+###
+import sys
+from gpiozero import Button
+from datetime import datetime
+
+### LOCAL
+try:
+    from .box_pin import BoxPin
+except ImportError as e:
+    print("Unable to import 'BoxPin' from 'utils.boxes.box_pin.py' file at 'utils.boxes.box_button.py' file")
+    sys.exit(1)
+
+
+class BoxButton(BoxPin):
+    """
+        TODO: Documentation
+    """
+
+    def __init__(self, configuration: dict, box_name: str, database: object, box_led: str = None) -> None:
+        """
+            TODO: Documentation
+        """
+
+        ### Init super
+        super().__init__(name=__name__, configuration=configuration, box_name=box_name)
+
+        self.logger.debug(f"__init__ - box_name: '{box_name}'")
+
+        self.box_led = box_led
+
+        self.database = database
+
+        self.set_pin_usage()
+
+    def set_pin_usage(self):
+        self.logger.debug(f"set_pin_usage - box_name: '{self.box_name}'")
+
+        self.button_press_count = 0
+        try:
+            self.bounce_time = float(self.configuration.box_button_bounce_time)
+
+        except Exception as e:
+            print(f"\n\n Error setting the bounce_time from button with exception: {e}")
+            sys.exit(1)
+        
+        self.button = Button(self.pin_button, bounce_time=self.bounce_time)
+
+    def update_db_from_activated(self):
+        self.logger.debug(f"update_db_from_activated - box_name '{self.box_name}' | pin_button: '{self.pin_button}'")
+
+        self.database.add_button_box_usage(box_name=self.box_name)
+
+    def activated(self):
+        self.logger.debug(f"activated - box_name '{self.box_name}' | pin_button: '{self.pin_button}' | count: '{self.button_press_count}'")
+        self.button_press_count += 1
+
+        ### Update DB
+        self.database.db.add_button_box_usage(box_name=self.box_name)
+
+        ### Set LED activation
+        if self.box_led:
+            self.box_led.activated()
diff --git a/gpio_buttons/utils/boxes/box_led.py b/gpio_buttons/utils/boxes/box_led.py
new file mode 100644
index 0000000..440edcf
--- /dev/null
+++ b/gpio_buttons/utils/boxes/box_led.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+"""
+   utils.database.from_api.py file for the c3 buttons.
+"""
+
+
+###
+### DEPENDENCIES
+###
+import sys
+import time
+from gpiozero import LED
+
+### LOCAL
+try:
+    from .box_pin import BoxPin
+except ImportError as e:
+    print("Unable to import 'BoxPin' from 'utils.boxes.box_pin.py' file at 'utils.boxes.box_button.py' file" )
+    sys.exit(1)
+
+
+class BoxLed(BoxPin):
+    """
+        TODO: Documentation
+    """
+
+    def __init__(self, configuration: dict, box_name: str) -> None:
+        """
+            TODO: Documentation
+        """
+
+        ### Init super
+        super().__init__(name=__name__, configuration=configuration, box_name=box_name)
+
+        self.logger.debug(f"__init__ - box_name: '{box_name}'")
+
+        self.blink_times = int(self.configuration.box_led_blink_times)
+
+        self.set_pin_usage()
+
+    def set_pin_usage(self):
+        self.logger.debug(f"set_pin_usage - box_name: '{self.box_name}'")
+
+        self.led = LED(self.pin_led)
+
+        if self.configuration.box_led_always_on:
+            self.led.on()
+
+    def activated(self):
+        self.logger.debug(f"activated - box_name: '{self.box_name}' | pin_led: '{self.pin_led}'")
+
+        i = 0
+        if self.configuration.box_led_always_on:
+            while i in range(self.blink_times):
+                self.led.off()
+                time.sleep(0.5)
+                self.led.on()
+                time.sleep(0.5)
+                i += 1
+
+            self.led.on()
+
+        else:
+            while i in range(self.blink_times):
+                self.led.on()
+                time.sleep(0.5)
+                self.led.off()
+                time.sleep(0.5)
+                i += 1
+
+            self.led.off()
+
diff --git a/gpio_buttons/utils/boxes/box_pin.py b/gpio_buttons/utils/boxes/box_pin.py
new file mode 100644
index 0000000..f4f800e
--- /dev/null
+++ b/gpio_buttons/utils/boxes/box_pin.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+"""
+   utils.configuration.from_file.py file for the c3 buttons.
+   This is the super class used from the other classes that fetches configurations from a file.
+"""
+import sys
+from abc import ABC, abstractmethod
+
+### LOCAL
+try:
+    from utils.configuration.app_logger import set_logger_from_config
+except ImportError as e:
+    print("Unable to import 'set_logger_from_dict' from 'utils.configuration.app_logger.py' file at 'utils.boxes.box_pin.py' file" )
+    sys.exit(1)
+
+
+class BoxPin(ABC):
+    """ TODO
+    """
+
+    def __init__(self, name: str, configuration: object, box_name: str) -> None:
+
+        self.configuration = configuration
+        self.logger = set_logger_from_config(name=name, config=self.configuration)
+        self.get_box_config(box_name)
+
+    def get_box_config(self, box_name):
+        self.logger.debug(f"get_box_config - box_name: {box_name}")
+
+        self.box_name = box_name
+        box_info = self.configuration.boxes.get(box_name)
+        self.pin_button = box_info.get('pin_button')
+        self.pin_led = box_info.get('pin_led')
+        self.enabled_led=self.configuration.box_led_enabled
+
+    @abstractmethod
+    def set_pin_usage(self):
+        pass
+
+    @abstractmethod
+    def activated(self):
+        pass
diff --git a/gpio_buttons/utils/configuration/__init__.py b/gpio_buttons/utils/configuration/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gpio_buttons/utils/configuration/app_configuration.py b/gpio_buttons/utils/configuration/app_configuration.py
new file mode 100644
index 0000000..0995a98
--- /dev/null
+++ b/gpio_buttons/utils/configuration/app_configuration.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+"""
+   utils.configuration.app_configuration.py file for the c3buttons system.
+   In this file the configuration will be setted from this order:
+        config.yml -> environment vars
+"""
+
+###
+### DEPENDENCIES
+###
+import sys
+
+### LOCAL
+try:
+    from .default_values import DEFAULT_VALUES
+except ImportError as e:
+    print("Unable to import 'utils.configuration.default_vals.py' from 'utils.configuration.app_configuration.py' file")
+    sys.exit(1)
+
+try:
+    from .app_logger import set_logger_from_default
+except ImportError as e:
+    print("Unable to import 'setup_logger' from 'utils.configuration.app_logger.py' file at the 'utils.configuration.app_configuration.py'")
+    sys.exit(1)
+
+try:
+    from .from_file_environment import ConfigurationFromDotEnv
+except ImportError as e:
+    print("Unable to import 'ConfigurationFromFile' from 'utils.configuration.from_file_environment.py' file at the 'utils.configuration.app_configuration.py'")
+    sys.exit(1)
+
+try:
+    from .from_file_yaml import ConfigurationFromYamlFile
+except ImportError as e:
+    print("Unable to import 'ConfigurationFromYamlFile' from 'utils.configuration.from_file_yaml.py' file at the 'utils.configuration.app_configuration.py'")
+    sys.exit(1)
+
+
+class AppConfiguration():
+    """
+        Class for setting the configuration of the app.
+    """
+
+    def __init__(self) -> None:
+        """
+            Function for initialiazing the AppConfiguration class.
+            Sets the class variables.
+
+            Returns
+            -------
+            None
+        """
+        self.logger = set_logger_from_default(name=__name__, values_default=DEFAULT_VALUES)
+
+        self.logger.info("---> STARTED")
+
+        self.__configuration_colors__()
+
+        self.set_attr_from_parsed()
+
+        if self.debug:
+            self.__debug_attrs__()
+
+    def check_missing_attrs(self, from_env: dict, from_yaml: dict) -> dict:
+
+        self.logger.debug("check_missin_attrs")
+
+        ret_dict = {}
+
+        # If in from_env and not in from_yaml
+        for name, value in from_env.dict_values.items():
+
+            if name not in from_yaml.dict_values.keys():
+                ret_dict[name] = value
+
+            # if name in from_yaml.dict_values.keys():
+            else:
+
+                if name == "boxes":
+                    yaml_value = from_yaml.dict_values.get("boxes")
+                    if len(value) >= len(yaml_value):
+                        boxes_dict = value
+                        boxes_list = from_env.dict_values.get("boxes_used")
+                        self.logger.info("---> Using boxes configuration from OS environments")
+
+                    else:
+                        boxes_dict = yaml_value
+                        boxes_list = from_yaml.dict_values.get("boxes_used")
+                        self.logger.info("---> Using boxes configuration from config.yml file")
+
+                    # ret_dict[name] = new_value
+                    ret_dict["boxes"] = boxes_dict
+                    ret_dict["boxes_used"] = boxes_list
+
+                elif name == "boxes_used":
+                    pass
+
+                else:
+                    ret_dict[name] = value
+
+
+            ### DEBUG
+            # from pprint import pp
+            # print(f"name: {name} | ret_dict: {ret_dict}")
+
+        # If in from_yaml and not in from_env
+        for name, value in from_yaml.dict_values.items():
+            if name not in ret_dict.keys():
+                ret_dict[name] = value
+
+        # from pprint import pp
+        # import pdb; pdb.set_trace()
+        # pp(ret_dict)
+
+        return ret_dict
+
+    def attr_set_from_dict(self, dict_values: dict) -> None:
+
+        self.logger.debug("attr_set_from_dict")
+
+        for name, value in dict_values.items():
+            setattr(self, name.lower(), value)
+
+    def set_attr_from_parsed(self) -> None:
+
+        self.logger.debug("set_attr_from_parsed")
+
+        ### Get attrs from env
+        from_env = ConfigurationFromDotEnv(default_values= DEFAULT_VALUES)
+
+        ### If not using, set it from the os.env
+        if not DEFAULT_VALUES.get("DEFAULT_USING_PARSED_YAML"):
+            dict_values = from_env.dict_values
+
+        else:
+            ### Fetch params from YAML file
+            from_yaml = ConfigurationFromYamlFile(default_values= DEFAULT_VALUES)
+            dict_values = self.check_missing_attrs(from_env=from_env, from_yaml=from_yaml)
+
+        ### Set the attributes as a dict aswell
+        self.attr_set_from_dict(dict_values)
+
+        self.dict_values = dict_values
+
+    def __configuration_colors__(self) -> None:
+        """
+            Method for setting the terminal output with colors.
+
+            Returns
+            -------
+            None
+        """
+        self.logger.debug("__configuration_colors__")
+
+        self.color_gray = "\033[90m"
+        self.color_red = "\033[91m"
+        self.color_green = "\033[92m"
+        self.color_orange = "\033[93m"
+        self.color_blue = "\033[94m"
+        self.color_pink = "\033[95m"
+        self.color_yellow = "\033[93m"
+        self.color_end = "\033[0m"
+
+    def __debug_attrs__(self) -> None:
+        """
+            Function for printing the configuration parameters from the app.
+
+            Returns
+            -------
+            None
+        """
+        self.logger.debug("__debug_attrs__")
+        ### PRINT ARGS
+        print("\n" + self.color_blue + "CONFIGURATION ATTRS:" + self.color_end)
+        for name, value in self.dict_values.items():
+            print(self.color_green + f"\t- {name}: " + self.color_end + f"'{value}'")
+
+        print("\n")
diff --git a/gpio_buttons/utils/configuration/app_logger.py b/gpio_buttons/utils/configuration/app_logger.py
new file mode 100644
index 0000000..6f3da52
--- /dev/null
+++ b/gpio_buttons/utils/configuration/app_logger.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python3
+"""
+   utils.configuration.app_logger.py file for the c3 printing system.
+"""
+
+###
+### DEPENDENCIES
+###
+import sys
+import logging
+
+
+def setup_logger(name: str,
+                 level: str | None = "INFO",
+                 log_formatter: str | None = None,
+                 log_file: str | None = None,
+                 log_file_append: bool | None = False
+                 ) -> logging.Logger:
+
+    if level == "DEBUG":
+        lvl = logging.DEBUG
+
+    elif level == "INFO":
+        lvl = logging.INFO
+
+    elif level == "WARNING":
+        lvl = logging.WARNING
+
+    elif level == "ERROR":
+        lvl = logging.ERROR
+
+    else:
+        print(f"\nERROR ---> Could not set logger for: {name}.\n LogLevel parsed '{level}' is unknown...\n\n")
+        sys.exit()
+
+    # Create a logger
+    logger = logging.getLogger(name)
+
+    # Set the logging level
+    logger.setLevel(lvl)
+
+    # Avoid adding multiple handlers
+    if not logger.hasHandlers():
+
+        # Set log formatter
+        if log_formatter is None:
+            formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+
+        else:
+            formatter = logging.Formatter(log_formatter)
+
+        if log_file is not None:
+            if log_file_append:
+                fh = logging.FileHandler(log_file, mode="a")
+            else:
+                fh = logging.FileHandler(log_file)
+
+            fh.setLevel(lvl)
+            fh.setFormatter(formatter)
+            logger.addHandler(fh)
+
+        # Create a console handler (optional, if you still want console output)
+        ch = logging.StreamHandler()
+        ch.setLevel(lvl)
+        # # Change to WARNING to only see warnings/errors in the terminal
+        # ch.setLevel(logging.WARNING)
+        # ch.setLevel(logging.INFO)
+
+        # Add formatter to both handlers
+        ch.setFormatter(formatter)
+
+        # Add handlers to the logger
+        logger.addHandler(ch)
+
+    return logger
+
+
+def set_logger_from_default(name: str, values_default: dict) -> logging.Logger:
+
+    log_file = None
+    if values_default.get('DEFAULT_LOG_TO_FILE'):
+        log_file = values_default.get('DEFAULT_LOG_FILE_PATH')
+
+    if values_default.get('DEFAULT_DEBUG'):
+        logger = setup_logger(
+            name=name,
+            level="DEBUG",
+            log_formatter=values_default.get('DEFAULT_LOG_FORMAT'),
+            log_file=log_file,
+        )
+
+    else:
+        logger = setup_logger(
+            name=name,
+            # level=logging.DEBUG,
+            log_formatter=values_default.get('DEFAULT_LOG_FORMAT'),
+            log_file=log_file,
+        )
+
+    return logger
+
+
+def set_logger_from_config(name: str, config: object) -> logging.Logger:
+
+    log_file = None
+    log_file_append = True
+
+    if config.log_to_file:
+        log_file = config.log_file_path
+        log_file_append = config.log_file_append_mode
+
+    if config.debug:
+        logger = setup_logger(
+            name=name,
+            level="DEBUG",
+            log_formatter=config.log_format,
+            log_file=log_file,
+            log_file_append=log_file_append,
+        )
+
+    else:
+        logger = setup_logger(
+            name=name,
+            # level="DEBUG",
+            log_formatter=config.log_format,
+            log_file=log_file,
+            log_file_append=log_file_append,
+        )
+
+    return logger
+
+
+def set_logger_from_dict(name: str, dict_values: dict) -> logging.Logger:
+
+    log_file = None
+    log_file_append = True
+    if dict_values.get('log_to_file'):
+        log_file = dict_values.get('log_file_path')
+        log_file_append = dict_values.get('log_file_append_mode')
+
+    if dict_values.get('debug'):
+        logger = setup_logger(
+            name=name,
+            level="DEBUG",
+            log_formatter=dict_values.get('log_format'),
+            log_file=log_file,
+            log_file_append=log_file_append,
+        )
+
+    else:
+        logger = setup_logger(
+            name=name,
+            # level="DEBUG",
+            log_formatter=dict_values.get('log_format'),
+            log_file=log_file,
+            log_file_append=log_file_append,
+        )
+
+    return logger
\ No newline at end of file
diff --git a/gpio_buttons/utils/configuration/config.yml b/gpio_buttons/utils/configuration/config.yml
new file mode 100644
index 0000000..21a79ee
--- /dev/null
+++ b/gpio_buttons/utils/configuration/config.yml
@@ -0,0 +1,27 @@
+---
+
+default:
+  debug: true
+  verbose: true
+  enabled_led: True
+  description: "Updated default app description"
+
+  boxes_used: "box_1, box_2"
+
+  box_1:
+    pin_button: 21
+    pin_led: 22
+
+  box_2:
+    pin_button: 23
+    pin_led: 23
+
+
+  ###
+  ### DEBUG
+  ###
+  foo: "bar"
+  # boxes_used: "box_1, box_2, box_3"
+  # box_3:
+  #   pin_button: 24
+  #   pin_led: 25
diff --git a/gpio_buttons/utils/configuration/default_values.py b/gpio_buttons/utils/configuration/default_values.py
new file mode 100644
index 0000000..13c9a88
--- /dev/null
+++ b/gpio_buttons/utils/configuration/default_values.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+"""
+   utils.config.default_vals.py file for the c3 buttons.
+   Used for setting the default values from the 'utils.app_configuration.py' file.
+"""
+
+DEFAULT_VALUES = {
+    'DEFAULT_DESCRIPTION': 'UPDATE_THIS',
+    # 'DEFAULT_PATH_CONFIG_ENV': './.env',
+    'DEFAULT_PATH_CONFIG_ENV': '.env',
+    ###
+    ### config.yml
+    ###
+    'DEFAULT_USING_PARSED_YAML': False,
+    'DEFAULT_PATH_CONFIG_YML': './utils/configuration/config.yml',
+    ###
+    ### DEBUG/VEBOSE
+    ###
+    'DEFAULT_DEBUG': False,
+    'DEFAULT_VERBOSE': False,
+    ###
+    ### LOG
+    ###
+    # "DEFAULT_LOG_TO_FILE": True,
+    "DEFAULT_LOG_TO_FILE": False,
+    "DEFAULT_LOG_FILE_APPEND_MODE": True,
+    "DEFAULT_LOG_FILE_PATH": "./logs/example.log",
+    "DEFAULT_LOG_FORMAT": '%(asctime)s - %(levelname)s - %(name)s - %(message)s',
+    ###
+    ### DB
+    ###
+    "DEFAULT_DATABASE_TYPE":"local_file",
+    "DEFAULT_DATABASE_TABLE_NAME_BUTTON_BOX": "button_box",
+    "DEFAULT_DATABASE_TABLE_NAME_BUTTON_BOX_USE": "button_box_use",
+    "DEFAULT_DATABASE_LOCAL_DB_FILE_PATH":"Please set --> DATABASE_LOCAL_DB_FILE_PATH",
+    "DEFAULT_DATABASE_API_URL":"http://localhost:8000",
+    "DEFAULT_DATABASE_API_TOKEN":"Please set --> DATABASE_API_TOKEN",
+    ###
+    ### BOXES CONFIG
+    ###
+    'DEFAULT_BOX_LED_ENABLED': False,
+    'DEFAULT_BOX_LED_AWAYS_ON': False,
+    'DEFAULT_BOX_LED_BLINK_TIMES': 5,
+
+    ### DEFAULT_BOX_BUTTON_BOUNCE_TIME in microseconds
+    'DEFAULT_BOX_BUTTON_BOUNCE_TIME': "0.1",
+    
+    ###
+    ### BOXES USED
+    ###
+    "DEFAULT_BOXES_USED": "DEFAULT_BOX_1, DEFAULT_BOX_2, DEFAULT_BOX_3, DEFAULT_BOX_4, DEFAULT_BOX_5",
+    "DEFAULT_BOX_1": '{"pin_button": 14, "pin_led": 15}',
+    "DEFAULT_BOX_2": '{"pin_button": 18, "pin_led": 23}',
+    "DEFAULT_BOX_3": '{"pin_button": 24, "pin_led": 25}',
+    "DEFAULT_BOX_4": '{"pin_button": 8, "pin_led": 7}',
+    "DEFAULT_BOX_5": '{"pin_button": 12, "pin_led": 16}',
+}
diff --git a/gpio_buttons/utils/configuration/from_file.py b/gpio_buttons/utils/configuration/from_file.py
new file mode 100644
index 0000000..b32f656
--- /dev/null
+++ b/gpio_buttons/utils/configuration/from_file.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+"""
+   utils.configuration.from_file.py file for the c3 buttons.
+   This is the super class used from the other classes that fetches configurations from a file.
+"""
+import sys
+from abc import ABC, abstractmethod
+
+### LOCAL
+try:
+    from .app_logger import set_logger_from_default
+except ImportError as e:
+    print("Unable to import 'setup_logger' from 'utils.app_logger.py' file at 'utils.configuration.from_file.py' file" )
+    sys.exit(1)
+
+
+class ConfigurationFromFile(ABC):
+    """ TODO
+    """
+
+    def __init__(self, name: str, default_values: dict) -> None:
+
+        self.default_values = default_values
+        # self.logger = set_logger_from_default(name=__name__, values_default=self.default_values)
+        self.logger = set_logger_from_default(name=name, values_default=self.default_values)
+
+        self.path_config_file = None
+        self.dict_values = {}
+
+    @abstractmethod
+    def get_values(self):
+        pass
diff --git a/gpio_buttons/utils/configuration/from_file_environment.py b/gpio_buttons/utils/configuration/from_file_environment.py
new file mode 100644
index 0000000..c8612a7
--- /dev/null
+++ b/gpio_buttons/utils/configuration/from_file_environment.py
@@ -0,0 +1,250 @@
+#!/usr/bin/env python3
+"""
+   utils.configuration.from_file.py file for the c3 buttons.
+"""
+
+###
+### DEPENDENCIES
+###
+import sys
+import os
+import json
+
+try:
+    from dotenv import load_dotenv
+except ImportError as e:
+    print("Unable to import 'dotenv'")
+    print("Please install it with 'pip(3) install dotenv --user'")
+    sys.exit(1)
+
+### LOCAL
+try:
+    from .from_file import ConfigurationFromFile
+except ImportError as e:
+    print("Unable to import 'ConfigurationFromFile' from 'utils.configuration.from_file.py' file at 'utils.configuration.from_file_environment.py' file" )
+    sys.exit(1)
+
+
+class ConfigurationFromDotEnv(ConfigurationFromFile):
+    """ TODO
+    """
+
+    def __init__(self, default_values: dict) -> None:
+        """
+            TODO: Documentation
+        """
+
+        ### Init super
+        super().__init__(name=__name__, default_values=default_values)
+
+        self.path_config_file = self.default_values.get('DEFAULT_PATH_CONFIG_ENV')
+        self.dict_values["boxes"] = {}
+        self.get_values()
+
+    def __get_env_bool__(self, name_config: str, name_config_default: str) -> bool:
+        """
+            Method to fetch the environment variable from a boolean type
+
+            Returns
+            -------
+            bool:
+                Return a boolean value from fetching the setted environment variable
+        """
+        self.logger.debug(f"__get_env_bool__ - {name_config}")
+        try:
+            fetched = os.getenv(name_config)
+
+            if fetched:
+                if fetched in ('YES', 'yes', 'true', 'True', 'TRUE'):
+                    ret_bool = True
+                elif fetched in ('NO', 'no', 'false', 'False', 'FALSE'):
+                    ret_bool = False
+                else:
+                    print(f"\n\n Unable to fetch the parsed environment '{name_config}'. It is set but not 'true' or 'false' \n\n")
+                    sys.exit(1)
+            else:
+                ret_bool = self.default_values.get(name_config_default)
+
+        except Exception as e:
+            print(f"\n\n Unable to fetch the parsed environment '{name_config}' \n| The exception is: '{e}' \n\n")
+            sys.exit(1)
+
+        return ret_bool
+
+    def __get_env__(self, name_config: str, name_config_default: str) -> (str | int):
+        """
+            Method to fetch the environment variable from a string or int type.
+
+            Returns
+            -------
+            str | int:
+                Return a the value from fetching the setted environment variable.
+        """
+        self.logger.debug(f"__get_env__ - {name_config}")
+        try:
+            fetched = os.getenv(name_config)
+
+            ### If not setted, get it from config
+            if fetched is None:
+                fetched = self.default_values.get(name_config_default)
+
+        except Exception as e:
+            print(f"\n\n Unable to fetch the parsed environment '{name_config}' \n| The exception is: '{e}' \n\n")
+            sys.exit(1)
+
+        return fetched
+
+    def __get_env_dict__(self, name_config: str, name_config_default: str):
+        """
+            Method to fetch the environment variable from a string or int type.
+
+            Returns
+            -------
+            str | int:
+                Return a the value from fetching the setted environment variable.
+        """
+        self.logger.debug(f"__get_env_dict__ - {name_config}")
+
+        fetched = self.__get_env__(
+            name_config=name_config,
+            name_config_default=name_config_default
+        )
+
+        try:
+            ret_dict = json.loads(fetched)
+
+        except Exception as e:
+            print(f"\n\n Unable to parse the dictionary environment from: '{name_config}' \n| The exception is: '{e}' \n\n")
+            sys.exit(1)
+
+        self.dict_values["boxes"][name_config.lower()] = ret_dict
+
+    def get_values(self) -> None:
+        """
+            Used for fetching the parsed values from the system.
+            If the value is not parsed, it will use the default values settend at: utils.configuration.default_vals.py
+        """
+        self.logger.debug("get_values")
+
+        if not load_dotenv(dotenv_path=self.path_config_file):
+            print("\n\n  ERROR!: \ņ\n  Unable to load the '.env' file at the path: '{self.path_config_file}'. Change it at the '.configuration.default_vals.py' file")
+            sys.exit()
+
+        self.dict_values["description"] = self.__get_env__(
+            name_config="DESCRIPTION",
+            name_config_default="DEFAULT_DESCRIPTION"
+        )
+        ###
+        ### DEBUG/VEBOSE
+        ###
+        self.dict_values["debug"] = self.__get_env_bool__(
+            name_config="DEBUG",
+            name_config_default="DEFAULT_DEBUG"
+        )
+        self.dict_values["verbose"] = self.__get_env_bool__(
+            name_config="VERBOSE",
+            name_config_default="DEFAULT_VERBOSE"
+        )
+        ###
+        ### LOG
+        ###
+        self.dict_values["log_file_append_mode"] = self.__get_env_bool__(
+            name_config="LOG_FILE_APPEND_MODE",
+            name_config_default="DEFAULT_LOG_FILE_APPEND_MODE"
+        )
+        self.dict_values["log_to_file"] = self.__get_env_bool__(
+            name_config="LOG_TO_FILE",
+            name_config_default="DEFAULT_LOG_TO_FILE"
+        )
+        self.dict_values["log_file_path"] = self.__get_env__(
+            name_config="LOG_FILE_PATH",
+            name_config_default="DEFAULT_LOG_FILE_PATH"
+        )
+        self.dict_values["log_format"] = self.__get_env__(
+            name_config="LOG_FORMAT",
+            name_config_default="DEFAULT_LOG_FORMAT"
+        )
+        ###
+        ### DB
+        ###
+        database_type = self.__get_env__(
+            name_config="DATABASE_TYPE",
+            name_config_default="DEFAULT_DATABASE_TYPE"
+        )
+
+        if database_type == "local_file":
+            self.dict_values["database_table_name_button_box"] = self.__get_env__(
+                name_config="DATABASE_TABLE_NAME_BUTTON_BOX",
+                name_config_default="DEFAULT_DATABASE_TABLE_NAME_BUTTON_BOX"
+            )
+            self.dict_values["database_table_name_button_box_use"] = self.__get_env__(
+                name_config="DATABASE_TABLE_NAME_BUTTON_BOX_USE",
+                name_config_default="DEFAULT_DATABASE_TABLE_NAME_BUTTON_BOX_USE"
+            )
+            self.dict_values["database_local_db_file_path"] = self.__get_env__(
+                name_config="DATABASE_LOCAL_DB_FILE_PATH",
+                name_config_default="DEFAULT_DATABASE_LOCAL_DB_FILE_PATH"
+            )
+
+        elif database_type == "api":
+            self.dict_values["database_api_url"] = self.__get_env__(
+                name_config="DATABASE_API_URL",
+                name_config_default="DEFAULT_DATABASE_API_URL"
+            )
+            self.dict_values["database_api_token"] = self.__get_env__(
+                name_config="DATABASE_API_TOKEN",
+                name_config_default="DEFAULT_DATABASE_API_TOKEN"
+            )
+
+        else:
+            print(f"\n\n Unable to set the database_type from '{database_type}'\n Accepted values are: 'local_file' or 'api' \n\n")
+            sys.exit(1)
+
+        self.dict_values["database_type"] = database_type        
+
+        ###
+        ### BOXES CONFIG
+        ###
+        self.dict_values["box_button_bounce_time"] = self.__get_env__(
+            name_config="BOX_BUTTON_BOUNCE_TIME",
+            name_config_default="DEFAULT_BOX_BUTTON_BOUNCE_TIME"
+        )
+        self.dict_values["box_led_enabled"] = self.__get_env_bool__(
+            name_config="BOX_LED_ENABLED",
+            name_config_default="DEFAULT_BOX_LED_ENABLED"
+        )
+        self.dict_values["box_led_always_on"] = self.__get_env_bool__(
+            name_config="BOX_LED_AWAYS_ON",
+            name_config_default="DEFAULT_BOX_LED_AWAYS_ON"
+        )        
+        self.dict_values["box_led_blink_times"] = self.__get_env__(
+            name_config="BOX_LED_BLINK_TIMES",
+            name_config_default="DEFAULT_BOX_LED_BLINK_TIMES"
+        )        
+        ###
+        ### BOXES USED
+        ###
+        # Set boxes_used as a list
+        self.dict_values["boxes_used"] = self.__get_env__(
+            name_config="BOXES_USED",
+            name_config_default="DEFAULT_BOXES_USED"
+        ).replace(' ','').replace("'", "").replace('"', '').split(',')
+
+
+        # import pdb; pdb.set_trace()
+        # self.dict_values["boxes_used"]
+        # os.getenv("BOXES_USED")
+        # load_dotenv(dotenv_path=self.path_config_file)
+        # # "'BUTTON_BOX_1, BUTTON_BOX_2'"
+
+        # Adds dict_values with 'boxes' infos from 'boxes_used' list
+        for box_name in self.dict_values.get("boxes_used"):
+
+            self.app_description = self.__get_env_dict__(
+                name_config=box_name,
+                name_config_default="DEFAULT_" + box_name
+            )
+
+        # Fix lower boxes names from boxes_used list
+        for index, item in enumerate(self.dict_values.get("boxes_used")):
+            self.dict_values.get("boxes_used")[index] = item.lower()
diff --git a/gpio_buttons/utils/configuration/from_file_yaml.py b/gpio_buttons/utils/configuration/from_file_yaml.py
new file mode 100644
index 0000000..0b37c9f
--- /dev/null
+++ b/gpio_buttons/utils/configuration/from_file_yaml.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+"""
+   utils.configuration.from_file_yaml.py file for the c3 buttons.
+"""
+
+###
+### DEPENDENCIES
+###
+import sys
+import json
+
+try:
+    import pyaml_env
+except ImportError as e:
+    print("Unable to import 'pyaml_env'")
+    print("Please install it with 'pip(3) install pyaml_env --user'")
+    sys.exit(1)
+
+### LOCAL
+try:
+    from .from_file import ConfigurationFromFile
+except ImportError as e:
+    print("Unable to import 'ConfigurationFromFile' from 'utils.configuration.from_file.py' file at the 'utils.configuration.from_file_yaml.py' file" )
+    sys.exit(1)
+
+
+class ConfigurationFromYamlFile(ConfigurationFromFile):
+
+    def __init__(self, default_values: dict) -> None:
+
+        ### Init super
+        super().__init__(name=__name__, default_values=default_values)
+
+        self.path_config_file = self.default_values.get('DEFAULT_PATH_CONFIG_YML')
+        self.dict_values = self.get_values()
+
+    def __get_env_dict__(self, parsed: dict) -> dict:
+
+        self.logger.debug(f"__get_env_dict__ - {parsed}")
+
+        parsed['boxes_used'] = parsed.get("boxes_used").replace(' ',''). split(',')
+        parsed['boxes'] = {}
+
+        for box_name in parsed.get("boxes_used"):
+
+            box_info = parsed.get(box_name)
+            if box_info is None:
+                print(f"\n\n Error getting the configuration from the box '{box_name}' from the 'config.yml' file. \n If you want to use this box, configure it also.\n\n")
+                sys.exit(1)
+
+            if isinstance(box_info, dict):
+                ret_dict = box_info
+
+            else:
+                try:
+                    ret_dict = json.loads(box_info)
+
+                except Exception as e:
+                    print(f"\n\n Unable to parse the dictionary from the '{box_name}' with value: '{box_info}' \n| The exception is: '{e}' \n\n")
+                    sys.exit(1)
+
+            parsed["boxes"][box_name.lower()] = ret_dict
+            parsed.pop(box_name)
+
+        return parsed
+
+    def get_values(self) -> dict:
+
+        self.logger.debug(f"get_values")
+
+        try:
+            parsed_all = pyaml_env.parse_config(self.path_config_file)
+
+        except Exception as e:
+            print(f"\n\n Error reading the 'yaml' configurtion file. \n Exception: {e}")
+            sys.exit(1)
+
+        default_parsed = parsed_all.get('default')
+        if default_parsed is None:
+            print(f"\n\n Error getting the configuration from the 'config.yml' file. \n Exception: {e}")
+            sys.exit(1)
+
+        return self.__get_env_dict__(parsed=default_parsed)
diff --git a/gpio_buttons/utils/database/__init__.py b/gpio_buttons/utils/database/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gpio_buttons/utils/database/app_database.py b/gpio_buttons/utils/database/app_database.py
new file mode 100644
index 0000000..d7047e6
--- /dev/null
+++ b/gpio_buttons/utils/database/app_database.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+"""
+   utils.database.app_database.py file for the c3 printing system.
+"""
+
+import sys
+import logging
+
+### LOCAL
+try:
+    from utils.configuration.app_logger import set_logger_from_config
+except ImportError as e:
+    print("Unable to import 'setup_logger' from 'utils.configuration.app_logger.py' file at 'utils.database.app_database.py' file")
+    sys.exit(1)
+
+try:
+    from .from_local_file import DataBaseLocalSqLite
+except ImportError as e:
+    print("Unable to import 'DataBaseUsed' from 'utils.database.from_local_file.py' file at 'utils.database.app_database.py' file")
+    sys.exit(1)
+
+try:
+    from .from_api import DataBaseApi
+except ImportError as e:
+    print("Unable to import 'DataBaseApi' from 'utils.database.from_api.py' file at 'utils.database.app_database.py' file")
+    sys.exit(1)
+
+
+class AppDatabase():
+    """
+        Class for setting the configuration of the app.
+    """
+
+    def __init__(self, configuration) -> None:
+        """
+            Function for initialiazing the AppConfiguration class.
+            Sets the class variables.
+
+            Returns
+            -------
+            None
+        """
+
+        self.configuration = configuration
+
+        self.logger = set_logger_from_config(name=__name__, config=self.configuration)
+
+        self.set_database()
+
+    def set_database(self) -> None:
+
+        self.logger.debug("set_database")
+
+        if self.configuration.database_type == "local_file":
+            self.logger.info("---> Using local SQLite3 file as Database")
+            self.db = DataBaseLocalSqLite(configuration=self.configuration)
+
+        elif self.configuration.database_type == "api":
+            self.logger.info("---> Using API calls as Database")
+            self.db = DataBaseApi(configuration=self.configuration)
+
+        else:
+            print(f"\n\n ERROR! ---> Unable to set database from the parsed 'database_type' with value: {self.configuration.database_type}")
+            sys.exit(1)
diff --git a/gpio_buttons/utils/database/from_api.py b/gpio_buttons/utils/database/from_api.py
new file mode 100644
index 0000000..3d9839e
--- /dev/null
+++ b/gpio_buttons/utils/database/from_api.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+"""
+   utils.database.from_api.py file for the c3 buttons.
+"""
+
+
+###
+### DEPENDENCIES
+###
+import sys
+import requests
+
+### LOCAL
+try:
+    from .from_database import DataBaseUsed
+except ImportError as e:
+    print("Unable to import 'DataBaseUsed' from 'utils.database.from_database.py' file at 'utils.database.from_api.py' file" )
+    sys.exit(1)
+
+
+class DataBaseApi(DataBaseUsed):
+    """
+        TODO: Documentation
+    """
+
+    def __init__(self, configuration: dict) -> None:
+        """
+            TODO: Documentation
+        """
+
+        ### Init super
+        super().__init__(name=__name__, configuration=configuration)
+
+        self.__check_token__()
+
+    def __check_token__(self) -> None:
+        """
+            TODO: Documentation
+        """
+
+        self.logger.debug("__check_token__")
+
+        response = self.get(endpoint="auth/test/closed")
+        if response is None:
+            self.logger.error("__check_token__ - Token is NOT working...")
+            print("\nERROR ---> Initial Token is not working.\n You should Generate a new one and set in the configuration...\n\n")
+            sys.exit()
+
+        else:
+            if response.get('error'):
+                print("\nERROR ---> Some error happend trying to get the initial token.\n ")
+                sys.exit()
+
+            else:
+                self.logger.info("__check_token__ - Token is working...")
+
+    def __get_token_headers__(self) -> dict:
+
+        return {
+            "Content-Type": "application/json; charset=utf-8",
+            "Authorization": f"Bearer {self.configuration.database_api_token}"
+        }
+
+    def get(self, endpoint, params=None) -> (dict | None):
+
+        self.logger.debug(f"get - endpoint: '{endpoint}' | params: '{params}'")
+
+        url = f"{self.configuration.database_api_url}/{endpoint}"
+        try:
+            response = requests.get(
+                url,
+                headers=self.__get_token_headers__(),
+                params=params
+            )
+            response.raise_for_status()
+            return response.json()
+
+        except requests.RequestException as e:
+            self.logger.error(f"GET request failed with exception: {e}")
+            return None
+
+    def post(self, endpoint, json=None, data=None) -> (dict | None):
+
+        self.logger.debug(f"post - endpoint: '{endpoint}' | json: '{json}' | data: '{data}'")
+
+        url = f"{self.configuration.database_api_url}/{endpoint}"
+        try:
+            response = requests.post(
+                url,
+                json=json,
+                data=data,
+                headers=self.__get_token_headers__()
+            )
+            response.raise_for_status()
+            return response.json()
+
+        except requests.RequestException as e:
+            self.logger.error(f"POST request failed with exception: {e}")
+            return None
+
+    def add_button_box_usage(self, box_name: str):
+
+        self.logger.debug(f"add_button_box_usage - box_name: '{box_name}'")
+
+        post_data = {
+            "name": box_name
+        }
+        resp = self.post(endpoint="api/box/use", json=post_data)
+        if resp.get('error'):
+            self.logger.error(f"Error adding button usage with response: {resp}")
diff --git a/gpio_buttons/utils/database/from_database.py b/gpio_buttons/utils/database/from_database.py
new file mode 100644
index 0000000..447d934
--- /dev/null
+++ b/gpio_buttons/utils/database/from_database.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+"""
+   utils.configuration.from_file.py file for the c3 buttons.
+   This is the super class used from the other classes that fetches configurations from a file.
+"""
+
+import sys
+from abc import ABC, abstractmethod
+
+try:
+    from utils.configuration.app_logger import set_logger_from_config
+except ImportError as e:
+    print("Unable to import 'set_logger_from_dict' from 'utils.configuration.app_logger.py' file at 'utils.database.from_api.py' file" )
+    sys.exit(1)
+
+class DataBaseUsed(ABC):
+    """ TODO
+    """
+
+    def __init__(self, name: str, configuration: dict) -> None:
+
+        self.configuration = configuration
+        # self.logger = set_logger_from_config(name=__name__, config=self.configuration)
+        self.logger = set_logger_from_config(name=name, config=self.configuration)
+        self.debug = configuration.debug
+        self.conn = None
+        self.cursor = None
+
+    @abstractmethod
+    def add_button_box_usage(self, box_name: str):
+        pass
diff --git a/gpio_buttons/utils/database/from_local_file.py b/gpio_buttons/utils/database/from_local_file.py
new file mode 100644
index 0000000..7017e52
--- /dev/null
+++ b/gpio_buttons/utils/database/from_local_file.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python3
+"""
+   utils.database.from_local_file.py file for the c3 buttons.
+"""
+
+###
+### DEPENDENCIES
+###
+import sys
+import sqlite3
+from datetime import datetime
+
+### LOCAL
+try:
+    from .from_database import DataBaseUsed
+except ImportError as e:
+    print("Unable to import 'DataBaseUsed' from 'utils.database.from_database.py' file at 'utils.database.from_local_file.py' file" )
+    sys.exit(1)
+
+
+class DataBaseLocalSqLite(DataBaseUsed):
+    """ TODO
+    """
+
+    def __init__(self, configuration: dict) -> None:
+
+        ### Init super
+        super().__init__(name=__name__, configuration=configuration)
+
+        self.file_path = self.configuration.database_local_db_file_path
+
+    ###
+    ### CONNECTION
+    ###
+    def conn_create(self):
+        self.conn = sqlite3.connect(self.file_path)
+        self.cursor = self.conn.cursor()
+
+    def conn_close(self):
+
+        if self.conn:
+            self.cursor.close()
+            self.conn.close()
+            self.cursor = None
+            self.conn = None
+
+    ###
+    ### QUERY
+    ###
+    def query_fetch_one(self, query: str) -> None:
+
+        # self.logger.debug(f"query_fetch_one - query: {query}")
+        self.logger.debug("query_fetch_one")
+
+        if self.conn is None:
+            self.conn_create()
+
+        try:
+            resp = self.cursor.execute(query)
+            return resp.fetchone()[0]
+
+        except Exception as e:
+            self.logger.error(f"ERROR executing query: {query} | with execption: {e}")
+
+    def query_fetch_all(self, query: str) -> None:
+
+        # self.logger.debug(f"query_fetch_all - query: {query}")
+        self.logger.debug("query_fetch_all")
+
+        if self.conn is None:
+            self.conn_create()
+
+        try:
+            self.cursor.execute(query)
+            resp = self.cursor.execute(query)
+
+            return resp.fetchall()
+
+        except Exception as e:
+            self.logger.error(f"ERROR executing query: {query} | with execption: {e}")
+
+        finally:
+            self.conn_close()
+
+    def query_exec(self, query: str) -> None:
+
+        # self.logger.debug(f"query_exec - query: {query}")
+        self.logger.debug("query_exec")
+
+        if self.conn is None:
+            self.conn_create()
+
+        try:
+            self.cursor.execute(query)
+
+        except Exception as e:
+            self.logger.error(f"ERROR executing query: {query} | with execption: {e}")
+
+        finally:
+            if self.conn:
+                self.conn.commit()
+
+    ###
+    ### TABLES
+    ###
+    def table_select_all(self, table_name: str) -> (list | None):
+
+        self.logger.debug(f"table_select_all - table_name: {table_name}")
+
+        if self.conn is None:
+            self.conn_create()
+
+        query = "SELECT * FROM " + table_name + ";"
+
+        return self.query_fetch_all(query=query)
+
+    def table_get_button_box_id(self, box_name: str):
+
+        self.logger.debug(f"table_get_button_box_id - box_name '{box_name}'")
+
+        table_name = self.configuration.database_table_name_button_box
+
+        query = """
+        SELECT id FROM {table_name} WHERE {field_name}="{field_value}";
+        """.format(
+            table_name=table_name,
+            field_name="name",
+            field_value=box_name
+        )
+
+        # box_id = self.query_fetch_one(query=query)
+        # self.conn_close()
+        # return box_id
+
+        ##################
+        ##################
+        ##################
+
+        return self.query_fetch_one(query=query)
+
+    def add_button_box_usage(self, box_name: str):
+
+        self.logger.debug(f"table_add_button_box_usage - box_name '{box_name}'")
+
+        if self.conn is None:
+            self.conn_create()
+
+        button_box_id = self.table_get_button_box_id(box_name=box_name)
+        values = (str(datetime.now()), button_box_id)
+
+        query = """
+            INSERT INTO "{table_name}" {columns_list} VALUES {values}
+        """.format(
+            table_name=self.configuration.database_table_name_button_box_use,
+            columns_list=("date", "box_used_id"),
+            values=values
+        )
+        self.query_exec(query)
+        self.conn_close()
diff --git a/pycodestyle.cfg b/pycodestyle.cfg
new file mode 100644
index 0000000..77841ab
--- /dev/null
+++ b/pycodestyle.cfg
@@ -0,0 +1,8 @@
+[pycodestyle]
+count = False
+; E266 too many leading '#' for block comment
+; E401 multiple imports on one line
+ignore = E266, E401
+max-line-length = 160
+; max-line-length = 81
+statistics = True
\ No newline at end of file
-- 
GitLab


From f532d6ac39891617b6c4fc45850db9a5ff4acbf0 Mon Sep 17 00:00:00 2001
From: fejao <mail@fejao.de>
Date: Tue, 18 Mar 2025 19:22:27 +0100
Subject: [PATCH 6/6] Removing auto mount volumes from docker-compose.yml

---
 docker/docker-compose.yml | 17 ++++++++---------
 1 file changed, 8 insertions(+), 9 deletions(-)

diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index dd06342..f67209a 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -42,10 +42,9 @@ services:
     privileged: true
     devices:
       - /dev/gpiomem:/dev/gpiomem
-    volumes:
-      # - /dev/gpiomem:/dev/gpiomem
-      ### DEBUG
-      - ../gpio_buttons:/c3buttons
+    # volumes:
+    #   ### DEBUG
+    #   - ../gpio_buttons:/c3buttons
     environment:
       ###
       ### PLEASE CHANGE THIS
@@ -74,8 +73,8 @@ services:
     ### DISABLE PORTS FOR PRODUCTION
     ports:
       - '3000:3000'
-    volumes:
-      - ./grafana/data:/var/lib/grafana
+    # volumes:
+    #   - ./grafana/data:/var/lib/grafana
     environment:
       - FOO=BAR
       - GF_FEATURE_TOGGLES_PUBLICDASHBOARDS=true
@@ -103,6 +102,6 @@ services:
       - '443:443'
       ### This is the admin port, disable for Production
       - '81:81'
-    volumes:
-      - ./proxy/data:/data
-      - ./proxy/letsencrypt:/etc/letsencrypt
+    # volumes:
+    #   - ./proxy/data:/data
+    #   - ./proxy/letsencrypt:/etc/letsencrypt
-- 
GitLab