diff --git a/iotDashboard/device_manager_client.py b/iotDashboard/device_manager_client.py index 4032af1..7d19f62 100644 --- a/iotDashboard/device_manager_client.py +++ b/iotDashboard/device_manager_client.py @@ -10,10 +10,15 @@ from datetime import datetime @dataclass class DeviceRegistrationResponse: device_id: str - certificate_id: str - certificate: str - private_key: str - ca_certificate: str + protocol: str + certificate_id: Optional[str] = None + ca_certificate_pem: Optional[str] = None + certificate_pem: Optional[str] = None + private_key_pem: Optional[str] = None + expires_at: Optional[datetime] = None + credential_id: Optional[str] = None + api_key: Optional[str] = None + webhook_secret: Optional[str] = None @dataclass @@ -21,9 +26,9 @@ class DeviceInfo: id: str name: str location: Optional[str] - is_active: bool + protocol: str + connection_config: Optional[Dict[str, Any]] created_at: datetime - certificates: List[Dict[str, Any]] class DeviceManagerAPIError(Exception): @@ -63,20 +68,33 @@ class DeviceManagerClient: status_code=0, message=f"Connection error: {str(e)}" ) - def register_device(self, name: str, location: Optional[str] = None) -> DeviceRegistrationResponse: - payload = {"name": name} + def register_device( + self, + name: str, + location: Optional[str] = None, + protocol: str = "mqtt", + connection_config: Optional[Dict[str, Any]] = None + ) -> DeviceRegistrationResponse: + payload = {"name": name, "protocol": protocol} if location: payload["location"] = location + if connection_config: + payload["connection_config"] = connection_config response = self._request("POST", "/devices/register", json=payload) data = response.json() return DeviceRegistrationResponse( device_id=data["device_id"], - certificate_id=data["certificate_id"], - certificate=data["certificate"], - private_key=data["private_key"], - ca_certificate=data["ca_certificate"], + protocol=data["protocol"], + certificate_id=data.get("certificate_id"), + ca_certificate_pem=data.get("ca_certificate_pem"), + certificate_pem=data.get("certificate_pem"), + private_key_pem=data.get("private_key_pem"), + expires_at=datetime.fromisoformat(data["expires_at"].replace("Z", "+00:00")) if data.get("expires_at") else None, + credential_id=data.get("credential_id"), + api_key=data.get("api_key"), + webhook_secret=data.get("webhook_secret"), ) def get_device(self, device_id: str) -> DeviceInfo: @@ -87,9 +105,9 @@ class DeviceManagerClient: id=data["id"], name=data["name"], location=data.get("location"), - is_active=data["is_active"], + protocol=data["protocol"], + connection_config=data.get("connection_config"), created_at=datetime.fromisoformat(data["created_at"].replace("Z", "+00:00")), - certificates=data.get("certificates", []), ) def list_devices(self) -> List[DeviceInfo]: @@ -101,11 +119,11 @@ class DeviceManagerClient: id=device["id"], name=device["name"], location=device.get("location"), - is_active=device["is_active"], + protocol=device["protocol"], + connection_config=device.get("connection_config"), created_at=datetime.fromisoformat( device["created_at"].replace("Z", "+00:00") ), - certificates=device.get("certificates", []), ) for device in data ] @@ -114,9 +132,22 @@ class DeviceManagerClient: response = self._request("POST", f"/devices/{device_id}/revoke") return response.json() - def renew_certificate(self, device_id: str) -> Dict[str, Any]: + def renew_certificate(self, device_id: str) -> DeviceRegistrationResponse: response = self._request("POST", f"/devices/{device_id}/renew") - return response.json() + data = response.json() + + return DeviceRegistrationResponse( + device_id=data["device_id"], + protocol=data["protocol"], + certificate_id=data.get("certificate_id"), + ca_certificate_pem=data.get("ca_certificate_pem"), + certificate_pem=data.get("certificate_pem"), + private_key_pem=data.get("private_key_pem"), + expires_at=datetime.fromisoformat(data["expires_at"].replace("Z", "+00:00")) if data.get("expires_at") else None, + credential_id=data.get("credential_id"), + api_key=data.get("api_key"), + webhook_secret=data.get("webhook_secret"), + ) def get_ca_certificate(self) -> str: response = self._request("GET", "/ca_certificate") @@ -137,8 +168,13 @@ class DeviceManagerClient: default_client = DeviceManagerClient() -def register_device(name: str, location: Optional[str] = None) -> DeviceRegistrationResponse: - return default_client.register_device(name, location) +def register_device( + name: str, + location: Optional[str] = None, + protocol: str = "mqtt", + connection_config: Optional[Dict[str, Any]] = None +) -> DeviceRegistrationResponse: + return default_client.register_device(name, location, protocol, connection_config) def get_device(device_id: str) -> DeviceInfo: diff --git a/iotDashboard/forms.py b/iotDashboard/forms.py index a8dc4f6..0a9e1ca 100644 --- a/iotDashboard/forms.py +++ b/iotDashboard/forms.py @@ -1,65 +1,40 @@ +""" +Django forms for the IoT Dashboard. + +Note: Device registration is handled through the device_manager API. +These forms are used for the legacy Django UI only. +""" + from django import forms -from iotDashboard.models import Device, Sensor, SensorType +from iotDashboard.models import Device class DeviceForm(forms.ModelForm): + """ + Form for creating/editing devices. + + Note: This is for the Django UI only. Actual device registration + happens through the device_manager microservice API. + """ + + protocol = forms.ChoiceField( + choices=[ + ("mqtt", "MQTT"), + ("http", "HTTP"), + ("webhook", "Webhook"), + ], + initial="mqtt", + help_text="Communication protocol for this device", + ) + class Meta: model = Device - fields = ["name", "ip", "protocol"] # Exclude sensors from the fields - - def __init__(self, *args, **kwargs): - # No need to handle sensors in the form - super(DeviceForm, self).__init__(*args, **kwargs) - - def save(self, commit=True): - # Save the device instance - device = super(DeviceForm, self).save(commit=False) - - if commit: - device.save() - - return device - - -class SensorWithTypeForm(forms.ModelForm): - # Add fields for SensorType directly in the form - type_name = forms.CharField(max_length=50, label="Sensor Type Name") - unit = forms.CharField(max_length=20, label="Unit", required=False) - protocol = forms.ChoiceField( - choices=[("mqtt", "MQTT"), ("http", "HTTP")], label="Protocol" - ) - topic = forms.CharField(max_length=100, label="Topic", required=False) - endpoint = forms.CharField(max_length=100, label="Endpoint", required=False) - - class Meta: - model = Sensor - fields = ["enabled"] # Exclude 'device' from the form fields - - def __init__(self, *args, **kwargs): - self.device = kwargs.pop("device", None) # Get the device from kwargs - super(SensorWithTypeForm, self).__init__(*args, **kwargs) - - def save(self, commit=True): - # Create or get the SensorType - try: - sensor_type = SensorType.objects.get(name=self.cleaned_data["type_name"]) - except SensorType.DoesNotExist: - sensor_type = SensorType( - name=self.cleaned_data["type_name"], - unit=self.cleaned_data["unit"], - protocol=self.cleaned_data["protocol"], - topic=self.cleaned_data["topic"], - endpoint=self.cleaned_data["endpoint"], - ) - if commit: - sensor_type.save() - - # Create Sensor with the SensorType found or created - sensor = super(SensorWithTypeForm, self).save(commit=False) - sensor.type = sensor_type - sensor.device = self.device # Associate the sensor with the device - - if commit: - sensor.save() - - return sensor + fields = ["name", "location", "protocol"] + widgets = { + "name": forms.TextInput(attrs={"class": "form-control", "placeholder": "Device name"}), + "location": forms.TextInput(attrs={"class": "form-control", "placeholder": "Device location (optional)"}), + } + help_texts = { + "name": "Unique identifier for this device", + "location": "Physical location or description", + } diff --git a/iotDashboard/main.py b/iotDashboard/main.py new file mode 100644 index 0000000..979affc --- /dev/null +++ b/iotDashboard/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from iotdashboard!") + + +if __name__ == "__main__": + main() diff --git a/iotDashboard/pyproject.toml b/iotDashboard/pyproject.toml new file mode 100644 index 0000000..7ff8b86 --- /dev/null +++ b/iotDashboard/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "dashboard" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [] diff --git a/iotDashboard/templates/sensor_form.html b/iotDashboard/templates/certificate_renew_confirm.html similarity index 51% rename from iotDashboard/templates/sensor_form.html rename to iotDashboard/templates/certificate_renew_confirm.html index ff1c4db..55d7732 100644 --- a/iotDashboard/templates/sensor_form.html +++ b/iotDashboard/templates/certificate_renew_confirm.html @@ -3,40 +3,39 @@ - Add Sensor and Type + Renew Certificate -
-

{% if form.instance.pk %}Edit{% else %}Add{% endif %} Device

+

Add New Device

+ + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %}
{% csrf_token %}
- - {{ form.name.label_tag }} {{ form.name }}
- {{ form.ip.label_tag }} {{ form.ip }}
- {{ form.protocol.label_tag }} {{ form.protocol }}
- - - {% if form.errors %} -
-
    - {% for field, errors in form.errors.items %} -
  • {{ field }}: {{ errors|join:", " }}
  • - {% endfor %} -
-
- {% endif %} + + +
+ +
+ + +
+ +
+ + +
MQTT devices will receive X.509 certificates for secure communication.
- - + Cancel
- - -
- {% if form.instance.pk %} - Add Sensor - Edit Sensors - {% endif %} -
diff --git a/iotDashboard/templates/device_list.html b/iotDashboard/templates/device_list.html index bfb8c57..9ec9d45 100644 --- a/iotDashboard/templates/device_list.html +++ b/iotDashboard/templates/device_list.html @@ -23,7 +23,7 @@ {% if user.is_authenticated %} {% else %} - + {% endif %} @@ -33,41 +33,61 @@

Manage Devices

+ + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + Add Device - + - + + - {% for device in devices %} + {% for device_data in devices %} - - - - + + + - + {% empty %} - + {% endfor %} diff --git a/iotDashboard/templates/sensor_confirm_delete.html b/iotDashboard/templates/sensor_confirm_delete.html deleted file mode 100644 index 1867eb6..0000000 --- a/iotDashboard/templates/sensor_confirm_delete.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - Confirm Delete Sensor - - - - - - - -
-

Confirm Delete Sensor

-
- Warning! Are you sure you want to delete the sensor "{{ sensor.type.name }}"? This action cannot be undone. -
-
- {% csrf_token %} -
- - Cancel -
- -
- - diff --git a/iotDashboard/templates/sensor_list.html b/iotDashboard/templates/sensor_list.html deleted file mode 100644 index dcf9182..0000000 --- a/iotDashboard/templates/sensor_list.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - Manage Sensors for {{ device.name }} - - - - - - - -
-

Sensors for {{ device.name }}

-
NameIP AddressLocation ProtocolSensor Types Certificate StatusCertificate Expiry Actions
{{ device.name }}{{ device.ip }}{{ device.protocol }}{{ device_data.device.name }}{{ device_data.device.location|default:"—" }}{{ device_data.device.protocol|upper }} - {% for sensor in device.sensors.all %} - {{ sensor.type.name }}{% if not forloop.last %}, {% endif %} - {% empty %} - No sensors - {% endfor %} + {% if device_data.device.protocol == 'mqtt' %} + {{ device_data.certificate_status }} + {% else %} + N/A + {% endif %} - Edit - Delete + {% if device_data.active_certificate %} + {{ device_data.active_certificate.expires_at|date:"Y-m-d H:i" }} + {% else %} + — + {% endif %} + + View + Delete + {% if device_data.device.protocol == 'mqtt' %} + Renew Cert + Revoke + {% endif %}
No devices found.No devices found.
- - - - - - - - - {% for sensor in sensors %} - - - - - - {% empty %} - - - - {% endfor %} - -
Sensor TypeEnabledActions
{{ sensor.type.name }}{{ sensor.enabled }} - Edit - Delete -
No sensors found for this device.
- -
- - diff --git a/iotDashboard/urls.py b/iotDashboard/urls.py index c94bee4..98feb5e 100644 --- a/iotDashboard/urls.py +++ b/iotDashboard/urls.py @@ -21,18 +21,25 @@ from iotDashboard import views urlpatterns = [ path("admin/", admin.site.urls), - path("devices_api/", views.devices_api), + + # Main dashboard path("", views.chart, name="index"), - path("fetch_device_data/", views.fetch_device_data, name="fetch_device_data"), + path("chart/", views.chart, name="chart"), + + # Device management path("devices/", views.device_list, name="device_list"), path("devices/add/", views.add_device, name="add_device"), - path("devices/edit//", views.edit_device, name="edit_device"), - path("devices/delete//", views.delete_device, name="delete_device"), + path("devices//", views.view_device, name="view_device"), + path("devices//delete/", views.delete_device, name="delete_device"), + + # Certificate management (MQTT devices only) + path("devices//certificate/revoke/", views.revoke_certificate, name="revoke_certificate"), + path("devices//certificate/renew/", views.renew_certificate, name="renew_certificate"), + + # Telemetry data API + path("fetch_device_data/", views.fetch_device_data, name="fetch_device_data"), + + # Legacy/utility endpoints + path("devices_api/", views.devices_api, name="devices_api"), path("logout/", views.logout_view, name="logout"), - path("sensors//", views.sensor_list, name="sensor_list"), - path("sensor/add/", views.add_sensor_with_type, name="add_sensor_with_type"), - # path('devices//sensors/', views.sensor_list, name='sensor_list'), - path("device//add_sensor/", views.add_sensor, name="add_sensor"), - path("sensor/edit//", views.edit_sensor, name="edit_sensor"), - path("sensor/delete//", views.delete_sensor, name="delete_sensor"), ] diff --git a/iotDashboard/views.py b/iotDashboard/views.py index edb538c..6c38d7c 100644 --- a/iotDashboard/views.py +++ b/iotDashboard/views.py @@ -1,235 +1,263 @@ -import redis import json -from django.db import connections -from django.http import JsonResponse, HttpResponse -from django.shortcuts import render, redirect, get_object_or_404 +from django.http import JsonResponse +from django.shortcuts import render, redirect +from django.contrib import messages -from .forms import DeviceForm, SensorWithTypeForm -from iotDashboard.models import Device, Sensor +from iotDashboard.models import Device, Telemetry +from iotDashboard.device_manager_client import DeviceManagerClient, DeviceManagerAPIError -redis_client = redis.StrictRedis(host="10.10.0.1", port=6379, db=0) +device_manager = DeviceManagerClient() -def fetch_gpt_data(): - return ( - redis_client.get("gpt") - .decode("utf-8") - .strip('b"') - .replace('\\"', '"') - .replace("\\n", "") - .replace("\\", "") - .replace("\\u00b0", "°") - ) +# def index(request): +# """Redirect to chart page.""" +# if request.user.is_authenticated: +# return redirect("/chart/") +# return HttpResponse("NOT AUTHENTICATED!!!") def chart(request): - # Fetch devices and their related sensors - devices = Device.objects.prefetch_related( - "sensors__type" - ).all() # Prefetch related sensors and their types - - # Create a list of devices and associated sensors - devices_json = [ - { - "name": device.name, - "sensors": [ - {"id": sensor.id, "type": sensor.type.name} - for sensor in device.sensors.all() - ], - } - for device in devices - ] - + """Main dashboard showing telemetry charts.""" try: - gpt_data = fetch_gpt_data() - gpt = json.loads(gpt_data) - except (redis.RedisError, json.JSONDecodeError) as e: - gpt = {"summary": "Error fetching data", "recommendations": {}} - print(f"Error fetching or parsing GPT data: {e}") - - context = { - "devices_json": json.dumps(devices_json), # Convert to a JSON string - "gpt": gpt, - } - - return render(request, "chart.html", context) + devices = Device.objects.all() + + devices_data = [] + for device in devices: + # Get unique metrics for this device from telemetry + metrics = ( + Telemetry.objects + .filter(device_id=device.id) + .values_list('metric', flat=True) + .distinct() + ) + + devices_data.append({ + "id": device.id, + "name": device.name, + "protocol": device.protocol, + "metrics": list(metrics), + }) + + context = { + "devices_json": json.dumps(devices_data), + } + + return render(request, "chart.html", context) + + except Exception as e: + messages.error(request, f"Error loading dashboard: {str(e)}") + return render(request, "chart.html", {"devices_json": "[]"}) def fetch_device_data(request): - device_name = request.GET.get("device", "Livingroom") - sensor_name = request.GET.get("sensor") # This will be the actual sensor name + """Fetch telemetry data for chart visualization.""" + from datetime import datetime, timedelta + from django.utils import timezone + + device_id = request.GET.get("device_id") + metric = request.GET.get("metric") start_date = request.GET.get("start_date") end_date = request.GET.get("end_date") - # Log the parameters to ensure they are correct - sensor_name = Sensor.objects.get(id=sensor_name).type.name + if not device_id: + return JsonResponse({"error": "device_id is required"}, status=400) - print("Device Name:", device_name) - print("Sensor Name:", sensor_name) # Log sensor name - print("Start Date:", start_date) - print("End Date:", end_date) - - # Get the specific device by name - device = get_object_or_404(Device, name=device_name) - - # Initialize lists to store times and values - times = [] - values = [] - - # Prepare SQL query and parameters for the device - query = """ - SELECT time, metric, value - FROM sensor_readings - WHERE device_name = %s - """ - params = [device.name] - - # If a specific sensor is specified, filter by that sensor name (converted to lowercase) - if sensor_name: - query += " AND metric = LOWER(%s)" # Convert to lowercase for comparison - params.append(sensor_name.lower()) # Convert sensor name to lowercase - - # Add time filtering to the query - if start_date: - query += " AND time >= %s::timestamptz" - params.append(start_date) - - if end_date: - query += " AND time <= %s::timestamptz" - params.append(end_date) - - # Log the final query and params - print("Final Query:", query) - print("Params Before Execution:", params) - - # Fetch data from the database - with connections["data"].cursor() as cursor: - cursor.execute(query, params) - rows = cursor.fetchall() - - # Log the number of rows returned - print("Number of Rows Returned:", len(rows)) - - # Process the results and extract times and values - for row in rows: - time, metric, value = row - formatted_time = time.strftime("%Y-%m-%d %H:%M:%S") - - times.append(formatted_time) - values.append(value) - - # If no data is found, return empty arrays - if not times and not values: - print("No data found for the specified device and sensor.") - return JsonResponse({"times": [], "values": []}) - - # Return the response in the expected format - return JsonResponse({"times": times, "values": values}) - - -def index(request): - if request.user.is_authenticated: - return redirect("/chart/") - return HttpResponse("NOT AUTHENTICATED!!!") + try: + # Build query using Django ORM + queryset = Telemetry.objects.filter(device_id=device_id) + + # Filter by metric if provided + if metric: + queryset = queryset.filter(metric=metric) + + # Parse and filter by date range (default to last 24 hours) + if start_date: + start_dt = datetime.fromisoformat(start_date.replace('Z', '+00:00')) + queryset = queryset.filter(time__gte=start_dt) + else: + # Default: last 24 hours + queryset = queryset.filter(time__gte=timezone.now() - timedelta(hours=24)) + + if end_date: + end_dt = datetime.fromisoformat(end_date.replace('Z', '+00:00')) + queryset = queryset.filter(time__lte=end_dt) + + # Order by time and get values + results = queryset.order_by('time').values_list('time', 'value') + + times = [] + values = [] + for time, value in results: + times.append(time.strftime("%Y-%m-%d %H:%M:%S")) + values.append(float(value)) + + return JsonResponse({"times": times, "values": values}) + + except Exception as e: + return JsonResponse({"error": str(e)}, status=500) def device_list(request): - devices = Device.objects.all() - return render(request, "device_list.html", {"devices": devices}) + """List all devices with their certificate status.""" + try: + devices = Device.objects.all() + + # Enrich devices with certificate information + devices_with_certs = [] + for device in devices: + device_data = { + "device": device, + "certificate_status": device.certificate_status if device.protocol == "mqtt" else "N/A", + "active_certificate": device.active_certificate if device.protocol == "mqtt" else None, + } + devices_with_certs.append(device_data) + + return render(request, "device_list.html", {"devices": devices_with_certs}) + + except Exception as e: + messages.error(request, f"Error loading devices: {str(e)}") + return render(request, "device_list.html", {"devices": []}) def add_device(request): + """Register a new device via device_manager API.""" if request.method == "POST": - form = DeviceForm(request.POST) - if form.is_valid(): - form.save() - return redirect("device_list") - else: - form = DeviceForm() - return render(request, "device_form.html", {"form": form}) + name = request.POST.get("name") + location = request.POST.get("location") + protocol = request.POST.get("protocol", "mqtt") + + if not name: + messages.error(request, "Device name is required") + return render(request, "device_form.html") + + try: + response = device_manager.register_device( + name=name, + location=location, + protocol=protocol + ) + + # Show credentials page (one-time view) + return render(request, "device_credentials.html", { + "device_name": name, + "response": response, + }) + + except DeviceManagerAPIError as e: + messages.error(request, f"Failed to register device: {e.message}") + return render(request, "device_form.html", { + "name": name, + "location": location, + "protocol": protocol, + }) + + return render(request, "device_form.html") -def edit_device(request, pk): - device = get_object_or_404(Device, pk=pk) - if request.method == "POST": - form = DeviceForm(request.POST, instance=device) - if form.is_valid(): - form.save() - return redirect("device_list") - else: - form = DeviceForm(instance=device) - return render(request, "device_form.html", {"form": form}) - - -def delete_device(request, pk): - device = get_object_or_404(Device, pk=pk) - if request.method == "POST": - device.delete() +def view_device(request, device_id): + """View device details and certificate information.""" + try: + device = Device.objects.get(id=device_id) + + # Get certificate if MQTT device + certificate = None + if device.protocol == "mqtt": + certificate = device.active_certificate + + context = { + "device": device, + "certificate": certificate, + } + + return render(request, "device_detail.html", context) + + except Device.DoesNotExist: + messages.error(request, f"Device {device_id} not found") + return redirect("device_list") + except Exception as e: + messages.error(request, f"Error loading device: {str(e)}") return redirect("device_list") - return render(request, "device_confirm_delete.html", {"device": device}) -def add_sensor_with_type(request): - if request.method == "POST": - form = SensorWithTypeForm(request.POST) - if form.is_valid(): - form.save() # This will save both Sensor and SensorType as needed - return redirect("device_list") # Adjust this to your specific URL name - else: - form = SensorWithTypeForm() +def delete_device(request, device_id): + """Delete a device.""" + try: + device = Device.objects.get(id=device_id) + + if request.method == "POST": + device_name = device.name + device.delete() + messages.success(request, f"Device '{device_name}' deleted successfully") + return redirect("device_list") + + return render(request, "device_confirm_delete.html", {"device": device}) + + except Device.DoesNotExist: + messages.error(request, f"Device {device_id} not found") + return redirect("device_list") - context = {"form": form} - return render(request, "sensor_form.html", context) + +def revoke_certificate(request, device_id): + """Revoke a device's certificate via device_manager API.""" + try: + device = Device.objects.get(id=device_id) + + if device.protocol != "mqtt": + messages.error(request, "Only MQTT devices have certificates to revoke") + return redirect("device_list") + + if request.method == "POST": + try: + device_manager.revoke_certificate(device_id) + messages.success(request, f"Certificate for device '{device.name}' revoked successfully") + except DeviceManagerAPIError as e: + messages.error(request, f"Failed to revoke certificate: {e.message}") + + return redirect("device_list") + + return render(request, "certificate_revoke_confirm.html", {"device": device}) + + except Device.DoesNotExist: + messages.error(request, f"Device {device_id} not found") + return redirect("device_list") + + +def renew_certificate(request, device_id): + """Renew a device's certificate via device_manager API.""" + try: + device = Device.objects.get(id=device_id) + + if device.protocol != "mqtt": + messages.error(request, "Only MQTT devices have certificates to renew") + return redirect("device_list") + + if request.method == "POST": + try: + response = device_manager.renew_certificate(device_id) + + # Show the new credentials (one-time view) + return render(request, "device_credentials.html", { + "device_name": device.name, + "response": response, + "is_renewal": True, + }) + except DeviceManagerAPIError as e: + messages.error(request, f"Failed to renew certificate: {e.message}") + return redirect("device_list") + + return render(request, "certificate_renew_confirm.html", {"device": device}) + + except Device.DoesNotExist: + messages.error(request, f"Device {device_id} not found") + return redirect("device_list") def logout_view(request): + """Redirect to admin logout.""" return redirect("/admin") def devices_api(request): - devices = list(Device.objects.all().values("name", "sensors__type__name")) + """JSON API endpoint for devices.""" + devices = list(Device.objects.all().values("id", "name", "protocol", "location")) return JsonResponse(devices, safe=False) - - -def sensor_list(request, device_id): - device = get_object_or_404(Device, id=device_id) - sensors = device.sensors.all() # Get sensors for this specific device - return render(request, "sensor_list.html", {"device": device, "sensors": sensors}) - - -def edit_sensor(request, pk): - sensor = get_object_or_404(Sensor, pk=pk) - if request.method == "POST": - form = SensorWithTypeForm(request.POST, instance=sensor) - if form.is_valid(): - form.save() - return redirect("sensor_list", device_id=sensor.device.pk) - else: - form = SensorWithTypeForm(instance=sensor) - return render(request, "sensor_form.html", {"form": form}) - - -def delete_sensor(request, pk): - sensor = get_object_or_404(Sensor, pk=pk) - if request.method == "POST": - device_id = sensor.device.pk - sensor.delete() - return redirect("sensor_list", device_id=device_id) - return render(request, "sensor_confirm_delete.html", {"sensor": sensor}) - - -def add_sensor(request, device_id): - device = get_object_or_404(Device, pk=device_id) - if request.method == "POST": - form = SensorWithTypeForm(request.POST) - if form.is_valid(): - sensor = form.save(commit=False) - sensor.device = device # Associate the sensor with the device - sensor.save() - return redirect( - "device_list" - ) # Redirect to device list or appropriate view - else: - form = SensorWithTypeForm() - - return render(request, "sensor_form.html", {"form": form, "device": device}) diff --git a/pyproject.toml b/pyproject.toml index 07d63af..af295a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,4 +23,6 @@ dev = [ [tool.uv.workspace] members = [ "db_migrations", + "iotDashboard", + "services/gpt_service", ] diff --git a/services/device_manager/app/app.py b/services/device_manager/app/app.py index 978684e..154a173 100644 --- a/services/device_manager/app/app.py +++ b/services/device_manager/app/app.py @@ -158,14 +158,20 @@ async def revoke_device_certificate(device_id: str): """ try: with get_db_context() as db: + # Get the active (non-revoked) certificate for the device device_cert = ( - db.query(DeviceCertificate).filter(DeviceCertificate.device_id == device_id).first() + db.query(DeviceCertificate) + .filter( + DeviceCertificate.device_id == device_id, + DeviceCertificate.revoked_at.is_(None) + ) + .first() ) if not device_cert: - raise HTTPException(status_code=404, detail="Device certificate not found") - - if device_cert.revoked_at: - raise HTTPException(status_code=400, detail="Certificate already revoked") + raise HTTPException( + status_code=404, + detail="No active certificate found for this device" + ) cert_manager.revoke_certificate(device_cert.certificate_pem) diff --git a/services/gpt_service/.env.sample b/services/gpt_service/.env.sample new file mode 100644 index 0000000..2e1a3db --- /dev/null +++ b/services/gpt_service/.env.sample @@ -0,0 +1,6 @@ +API_KEY = +PROVIDER_NAME = os.getenv("PROVIDER_NAME", "openai") +MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4.1") +# HOST_URL = os.getenv("MODEL_URL") +LOG_LEVEL=INFO + diff --git a/services/gpt_service/README.md b/services/gpt_service/README.md new file mode 100644 index 0000000..e69de29 diff --git a/services/gpt_service/config.py b/services/gpt_service/config.py new file mode 100644 index 0000000..261312c --- /dev/null +++ b/services/gpt_service/config.py @@ -0,0 +1,13 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +class Config: + """Configuration settings for the GPT Service.""" + + API_KEY = os.getenv("API_KEY") + PROVIDER_NAME = os.getenv("PROVIDER_NAME", "openai") + MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4") + HOST_URL = os.getenv("HOST_URL") + LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") diff --git a/services/gpt_service/gpt_service.py b/services/gpt_service/gpt_service.py new file mode 100644 index 0000000..96a2019 --- /dev/null +++ b/services/gpt_service/gpt_service.py @@ -0,0 +1,42 @@ +import openai + +from config import API_KEY, MODEL_NAME, PROVIDER_NAME, HOST_URL, LOG_LEVEL +import logging + +class GPTService: + def __init__(self): + self.api_key = API_KEY + self.model_name = MODEL_NAME + self.provider_name = PROVIDER_NAME + self.host_url = HOST_URL + + logging.basicConfig(level=getattr(logging, LOG_LEVEL.upper(), logging.INFO)) + self.logger = logging.getLogger(__name__) + + if self.provider_name == "openai": + openai.api_key = self.api_key + if self.host_url: + openai.api_base = self.host_url + self.logger.info(f"Initialized OpenAI GPTService with model {self.model_name}") + else: + self.logger.error(f"Unsupported provider: {self.provider_name}") + raise ValueError(f"Unsupported provider: {self.provider_name}") + + def analyze_metrics(self, metrics: dict) -> str: + """Analyze given metrics using GPT model and return insights.""" + prompt = f"Analyze the following metrics and provide insights:\n{metrics}" + try: + response = openai.Completion.create( + engine=self.model_name, + prompt=prompt, + max_tokens=150, + n=1, + stop=None, + temperature=0.7, + ) + insights = response.choices[0].text.strip() + self.logger.info("Successfully obtained insights from GPT model") + return insights + except Exception as e: + self.logger.error(f"Error during GPT analysis: {e}") + raise \ No newline at end of file diff --git a/services/gpt_service/main.py b/services/gpt_service/main.py new file mode 100644 index 0000000..99a4c28 --- /dev/null +++ b/services/gpt_service/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from gpt-service!") + + +if __name__ == "__main__": + main() diff --git a/services/gpt_service/pyproject.toml b/services/gpt_service/pyproject.toml new file mode 100644 index 0000000..5bb52f8 --- /dev/null +++ b/services/gpt_service/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "gpt-service" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [] diff --git a/uv.lock b/uv.lock index 769283f..40655b8 100644 --- a/uv.lock +++ b/uv.lock @@ -4,6 +4,7 @@ requires-python = ">=3.13" [manifest] members = [ + "dashboard", "db-migrations", "iotdashboard", ] @@ -112,6 +113,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "dashboard" +version = "0.1.0" +source = { virtual = "iotDashboard" } + [[package]] name = "db-migrations" version = "0.1.0"