Removed unused files, updated templates, added gpt service and small fixes on Django

This commit is contained in:
2025-11-03 13:02:15 +01:00
parent 96e2377073
commit 212b8d39a9
25 changed files with 861 additions and 492 deletions

View File

@@ -10,10 +10,15 @@ from datetime import datetime
@dataclass @dataclass
class DeviceRegistrationResponse: class DeviceRegistrationResponse:
device_id: str device_id: str
certificate_id: str protocol: str
certificate: str certificate_id: Optional[str] = None
private_key: str ca_certificate_pem: Optional[str] = None
ca_certificate: str 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 @dataclass
@@ -21,9 +26,9 @@ class DeviceInfo:
id: str id: str
name: str name: str
location: Optional[str] location: Optional[str]
is_active: bool protocol: str
connection_config: Optional[Dict[str, Any]]
created_at: datetime created_at: datetime
certificates: List[Dict[str, Any]]
class DeviceManagerAPIError(Exception): class DeviceManagerAPIError(Exception):
@@ -63,20 +68,33 @@ class DeviceManagerClient:
status_code=0, message=f"Connection error: {str(e)}" status_code=0, message=f"Connection error: {str(e)}"
) )
def register_device(self, name: str, location: Optional[str] = None) -> DeviceRegistrationResponse: def register_device(
payload = {"name": name} 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: if location:
payload["location"] = location payload["location"] = location
if connection_config:
payload["connection_config"] = connection_config
response = self._request("POST", "/devices/register", json=payload) response = self._request("POST", "/devices/register", json=payload)
data = response.json() data = response.json()
return DeviceRegistrationResponse( return DeviceRegistrationResponse(
device_id=data["device_id"], device_id=data["device_id"],
certificate_id=data["certificate_id"], protocol=data["protocol"],
certificate=data["certificate"], certificate_id=data.get("certificate_id"),
private_key=data["private_key"], ca_certificate_pem=data.get("ca_certificate_pem"),
ca_certificate=data["ca_certificate"], 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: def get_device(self, device_id: str) -> DeviceInfo:
@@ -87,9 +105,9 @@ class DeviceManagerClient:
id=data["id"], id=data["id"],
name=data["name"], name=data["name"],
location=data.get("location"), 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")), created_at=datetime.fromisoformat(data["created_at"].replace("Z", "+00:00")),
certificates=data.get("certificates", []),
) )
def list_devices(self) -> List[DeviceInfo]: def list_devices(self) -> List[DeviceInfo]:
@@ -101,11 +119,11 @@ class DeviceManagerClient:
id=device["id"], id=device["id"],
name=device["name"], name=device["name"],
location=device.get("location"), location=device.get("location"),
is_active=device["is_active"], protocol=device["protocol"],
connection_config=device.get("connection_config"),
created_at=datetime.fromisoformat( created_at=datetime.fromisoformat(
device["created_at"].replace("Z", "+00:00") device["created_at"].replace("Z", "+00:00")
), ),
certificates=device.get("certificates", []),
) )
for device in data for device in data
] ]
@@ -114,9 +132,22 @@ class DeviceManagerClient:
response = self._request("POST", f"/devices/{device_id}/revoke") response = self._request("POST", f"/devices/{device_id}/revoke")
return response.json() 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") 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: def get_ca_certificate(self) -> str:
response = self._request("GET", "/ca_certificate") response = self._request("GET", "/ca_certificate")
@@ -137,8 +168,13 @@ class DeviceManagerClient:
default_client = DeviceManagerClient() default_client = DeviceManagerClient()
def register_device(name: str, location: Optional[str] = None) -> DeviceRegistrationResponse: def register_device(
return default_client.register_device(name, location) 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: def get_device(device_id: str) -> DeviceInfo:

View File

@@ -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 django import forms
from iotDashboard.models import Device, Sensor, SensorType from iotDashboard.models import Device
class DeviceForm(forms.ModelForm): 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: class Meta:
model = Device model = Device
fields = ["name", "ip", "protocol"] # Exclude sensors from the fields fields = ["name", "location", "protocol"]
widgets = {
def __init__(self, *args, **kwargs): "name": forms.TextInput(attrs={"class": "form-control", "placeholder": "Device name"}),
# No need to handle sensors in the form "location": forms.TextInput(attrs={"class": "form-control", "placeholder": "Device location (optional)"}),
super(DeviceForm, self).__init__(*args, **kwargs) }
help_texts = {
def save(self, commit=True): "name": "Unique identifier for this device",
# Save the device instance "location": "Physical location or description",
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

6
iotDashboard/main.py Normal file
View File

@@ -0,0 +1,6 @@
def main():
print("Hello from iotdashboard!")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,5 @@
[project]
name = "dashboard"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = []

View File

@@ -3,15 +3,19 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add Sensor and Type</title> <title>Renew Certificate</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head> </head>
<body class="bg-light"> <body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-light bg-light"> <nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="#">IoT Dashboard</a> <a class="navbar-brand" href="#">IoT Dashboard</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="{% url 'index' %}">Chart</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'device_list' %}">Devices</a></li> <li class="nav-item"><a class="nav-link" href="{% url 'device_list' %}">Devices</a></li>
</ul> </ul>
</div> </div>
@@ -19,24 +23,19 @@
</nav> </nav>
<div class="container mt-5"> <div class="container mt-5">
<h1 class="text-center mb-4">Add Sensor and Sensor Type</h1> <h1 class="text-center mb-4">Renew Certificate</h1>
<form method="post">
<div class="alert alert-info" role="alert">
<strong>Info:</strong> This will generate a new certificate for device "{{ device.name }}" and revoke the old one.
</div>
<p class="text-center">Do you want to renew the certificate now?</p>
<form method="post" class="text-center">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} <!-- Renders the inputs for all fields you added in your form --> <button type="submit" class="btn btn-primary">Renew Certificate</button>
<input type="hidden" name="device_id" value="{{ device.id }}"> <!-- Hidden field for device ID -->
<button type="submit" class="btn btn-success">Save</button>
<a href="{% url 'device_list' %}" class="btn btn-secondary">Cancel</a> <a href="{% url 'device_list' %}" class="btn btn-secondary">Cancel</a>
</form> </form>
{% if form.errors %}
<div class="alert alert-danger">
<ul>
{% for field, errors in form.errors.items %}
<li>{{ field }}: {{ errors|join:', ' }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div> </div>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Revoke Certificate</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">IoT Dashboard</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="{% url 'index' %}">Chart</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'device_list' %}">Devices</a></li>
</ul>
</div>
</div>
</nav>
<div class="container mt-5">
<h1 class="text-center mb-4">Revoke Certificate</h1>
<div class="alert alert-warning" role="alert">
<strong>Warning!</strong> This action will revoke the certificate for device "{{ device.name }}".
The device will no longer be able to connect until a new certificate is issued.
</div>
<p class="text-center">Are you sure you want to revoke the certificate?</p>
<form method="post" class="text-center">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Revoke Certificate</button>
<a href="{% url 'device_list' %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
</body>
</html>

View File

@@ -93,19 +93,19 @@
<!-- Device Selector Dropdown --> <!-- Device Selector Dropdown -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-4 offset-md-4"> <div class="col-md-4 offset-md-4">
<select id="deviceSelector" class="form-select" onchange="updateSensorOptions()"> <select id="deviceSelector" class="form-select" onchange="updateMetricOptions()">
<option value="">-- Select a Device --</option> <option value="">-- Select a Device --</option>
<script> <script>
const devices = JSON.parse('{{ devices_json|escapejs }}'); const devices = JSON.parse('{{ devices_json|escapejs }}');
const deviceSelector = document.getElementById('deviceSelector'); const deviceSelector = document.getElementById('deviceSelector');
const sensorSelector = document.getElementById('sensorSelector'); const metricSelector = document.getElementById('metricSelector');
// Populate device selector // Populate device selector
devices.forEach(device => { devices.forEach(device => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = device.name; // Device name used for identification option.value = device.id; // Use device ID
option.textContent = device.name; option.textContent = device.name + ' (' + device.protocol.toUpperCase() + ')';
deviceSelector.appendChild(option); deviceSelector.appendChild(option);
}); });
</script> </script>
@@ -113,11 +113,11 @@
</div> </div>
</div> </div>
<!-- Sensor Selector Dropdown --> <!-- Metric Selector Dropdown -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-4 offset-md-4"> <div class="col-md-4 offset-md-4">
<select id="sensorSelector" class="form-select"> <select id="metricSelector" class="form-select">
<option value="">-- Select a Sensor --</option> <option value="">-- Select a Metric --</option>
</select> </select>
</div> </div>
</div> </div>
@@ -157,39 +157,39 @@
var ctx = document.getElementById('conditionsChart').getContext('2d'); var ctx = document.getElementById('conditionsChart').getContext('2d');
var conditionsChart; var conditionsChart;
function updateSensorOptions() { function updateMetricOptions() {
const selectedDeviceName = document.getElementById('deviceSelector').value; const selectedDeviceId = document.getElementById('deviceSelector').value;
// Find the selected device in the devices array // Find the selected device in the devices array
const selectedDevice = devices.find(device => device.name === selectedDeviceName); const selectedDevice = devices.find(device => device.id === selectedDeviceId);
// Clear the existing options in the sensorSelector // Clear the existing options in the metricSelector
const sensorSelector = document.getElementById('sensorSelector'); const metricSelector = document.getElementById('metricSelector');
sensorSelector.innerHTML = '<option value="">-- Select a Sensor --</option>'; metricSelector.innerHTML = '<option value="">-- Select a Metric --</option>';
if (selectedDevice) { if (selectedDevice && selectedDevice.metrics) {
// Populate the sensor selector with the sensors for the selected device // Populate the metric selector with the metrics for the selected device
selectedDevice.sensors.forEach(sensor => { selectedDevice.metrics.forEach(metric => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = sensor.id; // Set the sensor ID as the value option.value = metric;
option.textContent = sensor.type; // Set the sensor type as the display text option.textContent = metric.charAt(0).toUpperCase() + metric.slice(1);
sensorSelector.appendChild(option); metricSelector.appendChild(option);
}); });
} }
} }
function fetchDeviceData() { function fetchDeviceData() {
var device = document.getElementById('deviceSelector').value; var deviceId = document.getElementById('deviceSelector').value;
var sensor = document.getElementById('sensorSelector').value; // Get selected sensor var metric = document.getElementById('metricSelector').value;
var startDate = document.getElementById('startDate').value; var startDate = document.getElementById('startDate').value;
var endDate = document.getElementById('endDate').value; var endDate = document.getElementById('endDate').value;
if (!device || !sensor) { if (!deviceId || !metric) {
alert("Please select both a device and a sensor."); alert("Please select both a device and a metric.");
return; return;
} }
fetch(`/fetch_device_data/?device=${device}&sensor=${sensor}&start_date=${startDate}&end_date=${endDate}`) fetch(`/fetch_device_data/?device_id=${deviceId}&metric=${metric}&start_date=${startDate}&end_date=${endDate}`)
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
throw new Error("Network response was not ok: " + response.statusText); throw new Error("Network response was not ok: " + response.statusText);
@@ -212,9 +212,9 @@ function fetchDeviceData() {
labels: data.times, labels: data.times,
datasets: [ datasets: [
{ {
label: `${sensor} (°C or %)`, label: metric.charAt(0).toUpperCase() + metric.slice(1),
data: data.values, data: data.values,
borderColor: sensor === 'Temperature' ? 'red' : 'blue', borderColor: metric === 'temperature' ? 'red' : (metric === 'humidity' ? 'blue' : 'green'),
fill: false, fill: false,
} }
] ]

View File

@@ -29,7 +29,7 @@
</li> </li>
{% else %} {% else %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'login' %}">Login</a> <a class="nav-link" href="/admin">Login</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>

View File

@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Device Credentials - {{ device_name }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.credential-box {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 15px;
margin-bottom: 20px;
font-family: monospace;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 300px;
overflow-y: auto;
}
.copy-btn {
margin-top: 10px;
}
</style>
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">IoT Dashboard</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="{% url 'index' %}">Chart</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'device_list' %}">Devices</a></li>
</ul>
</div>
</div>
</nav>
<div class="container mt-5">
<div class="alert alert-success" role="alert">
<h4 class="alert-heading">✅ Device Registered Successfully!</h4>
<p>Device <strong>{{ device_name }}</strong> has been registered.</p>
</div>
<div class="alert alert-warning" role="alert">
<strong>⚠️ Important:</strong> Save these credentials now! They will not be shown again.
You'll need them to configure your device.
</div>
<div class="card mb-4">
<div class="card-header">
<h5>Device Information</h5>
</div>
<div class="card-body">
<table class="table">
<tr>
<th>Device ID:</th>
<td><code>{{ response.device_id }}</code></td>
</tr>
<tr>
<th>Protocol:</th>
<td><span class="badge bg-info">{{ response.protocol|upper }}</span></td>
</tr>
{% if response.certificate_id %}
<tr>
<th>Certificate ID:</th>
<td><code>{{ response.certificate_id }}</code></td>
</tr>
<tr>
<th>Expires At:</th>
<td>{{ response.expires_at|date:"Y-m-d H:i:s" }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
{% if response.protocol == 'mqtt' and response.certificate_pem %}
<div class="card mb-4">
<div class="card-header">
<h5>CA Certificate</h5>
</div>
<div class="card-body">
<div class="credential-box" id="ca-cert">{{ response.ca_certificate_pem }}</div>
<button class="btn btn-sm btn-secondary copy-btn" onclick="copyToClipboard('ca-cert')">Copy CA Certificate</button>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5>Device Certificate</h5>
</div>
<div class="card-body">
<div class="credential-box" id="device-cert">{{ response.certificate_pem }}</div>
<button class="btn btn-sm btn-secondary copy-btn" onclick="copyToClipboard('device-cert')">Copy Device Certificate</button>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5>Private Key</h5>
</div>
<div class="card-body">
<div class="credential-box" id="private-key">{{ response.private_key_pem }}</div>
<button class="btn btn-sm btn-secondary copy-btn" onclick="copyToClipboard('private-key')">Copy Private Key</button>
</div>
</div>
<div class="alert alert-info">
<h6>MQTT Configuration:</h6>
<ul>
<li>Broker: <code>localhost:8883</code></li>
<li>Topic: <code>devices/{{ response.device_id }}/&lt;metric&gt;</code></li>
<li>Example: <code>devices/{{ response.device_id }}/temperature</code></li>
</ul>
</div>
{% endif %}
{% if response.protocol == 'http' and response.api_key %}
<div class="card mb-4">
<div class="card-header">
<h5>API Key</h5>
</div>
<div class="card-body">
<div class="credential-box" id="api-key">{{ response.api_key }}</div>
<button class="btn btn-sm btn-secondary copy-btn" onclick="copyToClipboard('api-key')">Copy API Key</button>
</div>
</div>
{% endif %}
{% if response.protocol == 'webhook' and response.webhook_secret %}
<div class="card mb-4">
<div class="card-header">
<h5>Webhook Secret</h5>
</div>
<div class="card-body">
<div class="credential-box" id="webhook-secret">{{ response.webhook_secret }}</div>
<button class="btn btn-sm btn-secondary copy-btn" onclick="copyToClipboard('webhook-secret')">Copy Webhook Secret</button>
</div>
</div>
{% endif %}
<div class="text-center mt-4">
<a href="{% url 'device_list' %}" class="btn btn-primary">Go to Device List</a>
</div>
</div>
<script>
function copyToClipboard(elementId) {
const element = document.getElementById(elementId);
const text = element.textContent;
navigator.clipboard.writeText(text).then(() => {
// Change button text temporarily
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = '✓ Copied!';
btn.classList.remove('btn-secondary');
btn.classList.add('btn-success');
setTimeout(() => {
btn.textContent = originalText;
btn.classList.remove('btn-success');
btn.classList.add('btn-secondary');
}, 2000);
}).catch(err => {
alert('Failed to copy: ' + err);
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Device Details - {{ device.name }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">IoT Dashboard</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="{% url 'index' %}">Chart</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'device_list' %}">Devices</a></li>
</ul>
</div>
</div>
</nav>
<div class="container mt-5">
<h1 class="text-center mb-4">Device Details</h1>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<div class="card">
<div class="card-header">
<h3>{{ device.name }}</h3>
</div>
<div class="card-body">
<table class="table">
<tr>
<th>Device ID:</th>
<td><code>{{ device.id }}</code></td>
</tr>
<tr>
<th>Location:</th>
<td>{{ device.location|default:"—" }}</td>
</tr>
<tr>
<th>Protocol:</th>
<td><span class="badge bg-info">{{ device.protocol|upper }}</span></td>
</tr>
<tr>
<th>Created:</th>
<td>{{ device.created_at|date:"Y-m-d H:i:s" }}</td>
</tr>
</table>
{% if device.protocol == 'mqtt' and certificate %}
<h4 class="mt-4">Certificate Information</h4>
<table class="table">
<tr>
<th>Certificate ID:</th>
<td><code>{{ certificate.id }}</code></td>
</tr>
<tr>
<th>Issued At:</th>
<td>{{ certificate.issued_at|date:"Y-m-d H:i:s" }}</td>
</tr>
<tr>
<th>Expires At:</th>
<td>{{ certificate.expires_at|date:"Y-m-d H:i:s" }}</td>
</tr>
<tr>
<th>Status:</th>
<td>
{% if certificate.revoked_at %}
<span class="badge bg-danger">Revoked</span>
{% else %}
<span class="badge bg-success">Active</span>
{% endif %}
</td>
</tr>
</table>
{% endif %}
<div class="mt-4">
<a href="{% url 'device_list' %}" class="btn btn-secondary">Back to List</a>
{% if device.protocol == 'mqtt' %}
<a href="{% url 'renew_certificate' device.id %}" class="btn btn-warning">Renew Certificate</a>
<a href="{% url 'revoke_certificate' device.id %}" class="btn btn-danger">Revoke Certificate</a>
{% endif %}
<a href="{% url 'delete_device' device.id %}" class="btn btn-danger">Delete Device</a>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -23,40 +23,42 @@
</nav> </nav>
<div class="container mt-5"> <div class="container mt-5">
<h1 class="text-center mb-4">{% if form.instance.pk %}Edit{% else %}Add{% endif %} Device</h1> <h1 class="text-center mb-4">Add New Device</h1>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<div class="mb-3"> <div class="mb-3">
<!-- Display the form fields --> <label for="name" class="form-label">Device Name *</label>
{{ form.name.label_tag }} {{ form.name }}<br> <input type="text" class="form-control" id="name" name="name" value="{{ name }}" required>
{{ form.ip.label_tag }} {{ form.ip }}<br>
{{ form.protocol.label_tag }} {{ form.protocol }}<br>
<!-- If there are errors, display them -->
{% if form.errors %}
<div class="alert alert-danger">
<ul>
{% for field, errors in form.errors.items %}
<li>{{ field }}: {{ errors|join:", " }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div> </div>
<!-- Submit and Cancel buttons --> <div class="mb-3">
<button type="submit" class="btn btn-success">Save</button> <label for="location" class="form-label">Location</label>
<input type="text" class="form-control" id="location" name="location" value="{{ location }}">
</div>
<div class="mb-3">
<label for="protocol" class="form-label">Protocol *</label>
<select class="form-select" id="protocol" name="protocol" required>
<option value="mqtt" {% if protocol == 'mqtt' %}selected{% endif %}>MQTT (with mTLS)</option>
<option value="http" {% if protocol == 'http' %}selected{% endif %}>HTTP (coming soon)</option>
<option value="webhook" {% if protocol == 'webhook' %}selected{% endif %}>Webhook (coming soon)</option>
</select>
<div class="form-text">MQTT devices will receive X.509 certificates for secure communication.</div>
</div>
<button type="submit" class="btn btn-success">Register Device</button>
<a href="{% url 'device_list' %}" class="btn btn-secondary">Cancel</a> <a href="{% url 'device_list' %}" class="btn btn-secondary">Cancel</a>
</form> </form>
<!-- Links for adding/editing sensors for this specific device -->
<div class="mt-4">
{% if form.instance.pk %}
<a href="{% url 'add_sensor' form.instance.pk %}" class="btn btn-primary">Add Sensor</a>
<a href="{% url 'sensor_list' form.instance.pk %}" class="btn btn-info">Edit Sensors</a>
{% endif %}
</div>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -23,7 +23,7 @@
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li class="nav-item"><a class="nav-link" href="{% url 'logout' %}">Logout</a></li> <li class="nav-item"><a class="nav-link" href="{% url 'logout' %}">Logout</a></li>
{% else %} {% else %}
<li class="nav-item"><a class="nav-link" href="{% url 'login' %}">Login</a></li> <li class="nav-item"><a class="nav-link" href="/admin">Login</a></li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
@@ -33,41 +33,61 @@
<!-- Device Management --> <!-- Device Management -->
<div class="container mt-5"> <div class="container mt-5">
<h1 class="text-center mb-4">Manage Devices</h1> <h1 class="text-center mb-4">Manage Devices</h1>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<a href="{% url 'add_device' %}" class="btn btn-primary mb-3">Add Device</a> <a href="{% url 'add_device' %}" class="btn btn-primary mb-3">Add Device</a>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>IP Address</th> <th>Location</th>
<th>Protocol</th> <th>Protocol</th>
<th>Sensor Types</th> <!-- Adjusted to Sensor Types --> <th>Certificate Status</th>
<th>Certificate Expiry</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for device in devices %} {% for device_data in devices %}
<tr> <tr>
<td>{{ device.name }}</td> <td>{{ device_data.device.name }}</td>
<td>{{ device.ip }}</td> <td>{{ device_data.device.location|default:"—" }}</td>
<td>{{ device.protocol }}</td> <td><span class="badge bg-info">{{ device_data.device.protocol|upper }}</span></td>
<!-- Collect and display sensor types -->
<td> <td>
{% for sensor in device.sensors.all %} {% if device_data.device.protocol == 'mqtt' %}
{{ sensor.type.name }}{% if not forloop.last %}, {% endif %} <span class="badge bg-success">{{ device_data.certificate_status }}</span>
{% empty %} {% else %}
No sensors <span class="badge bg-secondary">N/A</span>
{% endfor %} {% endif %}
</td> </td>
<!-- You may further expand other sensor-related data -->
<td> <td>
<a href="{% url 'edit_device' device.pk %}" class="btn btn-warning btn-sm">Edit</a> {% if device_data.active_certificate %}
<a href="{% url 'delete_device' device.pk %}" class="btn btn-danger btn-sm">Delete</a> {{ device_data.active_certificate.expires_at|date:"Y-m-d H:i" }}
{% else %}
{% endif %}
</td>
<td>
<a href="{% url 'view_device' device_data.device.id %}" class="btn btn-info btn-sm">View</a>
<a href="{% url 'delete_device' device_data.device.id %}" class="btn btn-danger btn-sm">Delete</a>
{% if device_data.device.protocol == 'mqtt' %}
<a href="{% url 'renew_certificate' device_data.device.id %}" class="btn btn-warning btn-sm">Renew Cert</a>
<a href="{% url 'revoke_certificate' device_data.device.id %}" class="btn btn-secondary btn-sm">Revoke</a>
{% endif %}
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="5" class="text-center">No devices found.</td> <td colspan="6" class="text-center">No devices found.</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -1,48 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Confirm Delete Sensor</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">IoT Dashboard</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="{% url 'index' %}">Chart</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'device_list' %}">Devices</a></li>
</ul>
<ul class="navbar-nav">
{% if user.is_authenticated %}
<li class="nav-item"><a class="nav-link" href="{% url 'logout' %}">Logout</a></li>
{% else %}
<li class="nav-item"><a class="nav-link" href="{% url 'login' %}">Login</a></li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- Delete Confirmation -->
<div class="container mt-5">
<h1 class="text-center mb-4">Confirm Delete Sensor</h1>
<div class="alert alert-warning">
<strong>Warning!</strong> Are you sure you want to delete the sensor "{{ sensor.type.name }}"? This action cannot be undone.
</div>
<form method="POST">
{% csrf_token %}
<div class="text-center">
<button type="submit" class="btn btn-danger">Delete</button>
<a href="{% url 'sensor_list' sensor.device.pk %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</body>
</html>

View File

@@ -1,64 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manage Sensors for {{ device.name }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">IoT Dashboard</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="{% url 'index' %}">Chart</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'device_list' %}">Devices</a></li>
</ul>
<ul class="navbar-nav">
{% if user.is_authenticated %}
<li class="nav-item"><a class="nav-link" href="{% url 'logout' %}">Logout</a></li>
{% else %}
<li class="nav-item"><a class="nav-link" href="{% url 'login' %}">Login</a></li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- Sensor Management -->
<div class="container mt-5">
<h1>Sensors for {{ device.name }}</h1>
<table class="table">
<thead>
<tr>
<th>Sensor Type</th>
<th>Enabled</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for sensor in sensors %}
<tr>
<td>{{ sensor.type.name }}</td>
<td>{{ sensor.enabled }}</td>
<td>
<a href="{% url 'edit_sensor' sensor.pk %}" class="btn btn-warning btn-sm">Edit</a>
<a href="{% url 'delete_sensor' sensor.pk %}" class="btn btn-danger btn-sm">Delete</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="3">No sensors found for this device.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</body>
</html>

View File

@@ -21,18 +21,25 @@ from iotDashboard import views
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("devices_api/", views.devices_api),
# Main dashboard
path("", views.chart, name="index"), 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/", views.device_list, name="device_list"),
path("devices/add/", views.add_device, name="add_device"), path("devices/add/", views.add_device, name="add_device"),
path("devices/edit/<int:pk>/", views.edit_device, name="edit_device"), path("devices/<str:device_id>/", views.view_device, name="view_device"),
path("devices/delete/<int:pk>/", views.delete_device, name="delete_device"), path("devices/<str:device_id>/delete/", views.delete_device, name="delete_device"),
# Certificate management (MQTT devices only)
path("devices/<str:device_id>/certificate/revoke/", views.revoke_certificate, name="revoke_certificate"),
path("devices/<str:device_id>/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("logout/", views.logout_view, name="logout"),
path("sensors/<int:device_id>/", views.sensor_list, name="sensor_list"),
path("sensor/add/", views.add_sensor_with_type, name="add_sensor_with_type"),
# path('devices/<int:device_id>/sensors/', views.sensor_list, name='sensor_list'),
path("device/<int:device_id>/add_sensor/", views.add_sensor, name="add_sensor"),
path("sensor/edit/<int:pk>/", views.edit_sensor, name="edit_sensor"),
path("sensor/delete/<int:pk>/", views.delete_sensor, name="delete_sensor"),
] ]

View File

@@ -1,235 +1,263 @@
import redis
import json import json
from django.db import connections from django.http import JsonResponse
from django.http import JsonResponse, HttpResponse from django.shortcuts import render, redirect
from django.shortcuts import render, redirect, get_object_or_404 from django.contrib import messages
from .forms import DeviceForm, SensorWithTypeForm from iotDashboard.models import Device, Telemetry
from iotDashboard.models import Device, Sensor 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(): # def index(request):
return ( # """Redirect to chart page."""
redis_client.get("gpt") # if request.user.is_authenticated:
.decode("utf-8") # return redirect("/chart/")
.strip('b"') # return HttpResponse("NOT AUTHENTICATED!!!")
.replace('\\"', '"')
.replace("\\n", "")
.replace("\\", "")
.replace("\\u00b0", "°")
)
def chart(request): def chart(request):
# Fetch devices and their related sensors """Main dashboard showing telemetry charts."""
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
]
try: try:
gpt_data = fetch_gpt_data() devices = Device.objects.all()
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_data = []
"devices_json": json.dumps(devices_json), # Convert to a JSON string for device in devices:
"gpt": gpt, # Get unique metrics for this device from telemetry
} metrics = (
Telemetry.objects
.filter(device_id=device.id)
.values_list('metric', flat=True)
.distinct()
)
return render(request, "chart.html", context) 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): def fetch_device_data(request):
device_name = request.GET.get("device", "Livingroom") """Fetch telemetry data for chart visualization."""
sensor_name = request.GET.get("sensor") # This will be the actual sensor name 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") start_date = request.GET.get("start_date")
end_date = request.GET.get("end_date") end_date = request.GET.get("end_date")
# Log the parameters to ensure they are correct if not device_id:
sensor_name = Sensor.objects.get(id=sensor_name).type.name return JsonResponse({"error": "device_id is required"}, status=400)
print("Device Name:", device_name) try:
print("Sensor Name:", sensor_name) # Log sensor name # Build query using Django ORM
print("Start Date:", start_date) queryset = Telemetry.objects.filter(device_id=device_id)
print("End Date:", end_date)
# Get the specific device by name # Filter by metric if provided
device = get_object_or_404(Device, name=device_name) if metric:
queryset = queryset.filter(metric=metric)
# Initialize lists to store times and values # Parse and filter by date range (default to last 24 hours)
times = [] if start_date:
values = [] 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))
# Prepare SQL query and parameters for the device if end_date:
query = """ end_dt = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
SELECT time, metric, value queryset = queryset.filter(time__lte=end_dt)
FROM sensor_readings
WHERE device_name = %s
"""
params = [device.name]
# If a specific sensor is specified, filter by that sensor name (converted to lowercase) # Order by time and get values
if sensor_name: results = queryset.order_by('time').values_list('time', 'value')
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 times = []
if start_date: values = []
query += " AND time >= %s::timestamptz" for time, value in results:
params.append(start_date) times.append(time.strftime("%Y-%m-%d %H:%M:%S"))
values.append(float(value))
if end_date: return JsonResponse({"times": times, "values": values})
query += " AND time <= %s::timestamptz"
params.append(end_date)
# Log the final query and params except Exception as e:
print("Final Query:", query) return JsonResponse({"error": str(e)}, status=500)
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!!!")
def device_list(request): def device_list(request):
devices = Device.objects.all() """List all devices with their certificate status."""
return render(request, "device_list.html", {"devices": devices}) 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): def add_device(request):
"""Register a new device via device_manager API."""
if request.method == "POST": if request.method == "POST":
form = DeviceForm(request.POST) name = request.POST.get("name")
if form.is_valid(): location = request.POST.get("location")
form.save() protocol = request.POST.get("protocol", "mqtt")
return redirect("device_list")
else: if not name:
form = DeviceForm() messages.error(request, "Device name is required")
return render(request, "device_form.html", {"form": form}) 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): def view_device(request, device_id):
device = get_object_or_404(Device, pk=pk) """View device details and certificate information."""
if request.method == "POST": try:
form = DeviceForm(request.POST, instance=device) device = Device.objects.get(id=device_id)
if form.is_valid():
form.save()
return redirect("device_list")
else:
form = DeviceForm(instance=device)
return render(request, "device_form.html", {"form": form})
# Get certificate if MQTT device
certificate = None
if device.protocol == "mqtt":
certificate = device.active_certificate
def delete_device(request, pk): context = {
device = get_object_or_404(Device, pk=pk) "device": device,
if request.method == "POST": "certificate": certificate,
device.delete() }
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 redirect("device_list")
return render(request, "device_confirm_delete.html", {"device": device})
def add_sensor_with_type(request): def delete_device(request, device_id):
if request.method == "POST": """Delete a device."""
form = SensorWithTypeForm(request.POST) try:
if form.is_valid(): device = Device.objects.get(id=device_id)
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()
context = {"form": form} if request.method == "POST":
return render(request, "sensor_form.html", context) 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")
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): def logout_view(request):
"""Redirect to admin logout."""
return redirect("/admin") return redirect("/admin")
def devices_api(request): 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) 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})

View File

@@ -23,4 +23,6 @@ dev = [
[tool.uv.workspace] [tool.uv.workspace]
members = [ members = [
"db_migrations", "db_migrations",
"iotDashboard",
"services/gpt_service",
] ]

View File

@@ -158,14 +158,20 @@ async def revoke_device_certificate(device_id: str):
""" """
try: try:
with get_db_context() as db: with get_db_context() as db:
# Get the active (non-revoked) certificate for the device
device_cert = ( 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: if not device_cert:
raise HTTPException(status_code=404, detail="Device certificate not found") raise HTTPException(
status_code=404,
if device_cert.revoked_at: detail="No active certificate found for this device"
raise HTTPException(status_code=400, detail="Certificate already revoked") )
cert_manager.revoke_certificate(device_cert.certificate_pem) cert_manager.revoke_certificate(device_cert.certificate_pem)

View File

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

View File

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
def main():
print("Hello from gpt-service!")
if __name__ == "__main__":
main()

View File

@@ -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 = []

6
uv.lock generated
View File

@@ -4,6 +4,7 @@ requires-python = ">=3.13"
[manifest] [manifest]
members = [ members = [
"dashboard",
"db-migrations", "db-migrations",
"iotdashboard", "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" }, { 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]] [[package]]
name = "db-migrations" name = "db-migrations"
version = "0.1.0" version = "0.1.0"