Skip to content

Commit 4e01ba3

Browse files
committed
commit Fix database initialization and improve API error handling
1 parent 00bccd6 commit 4e01ba3

File tree

7 files changed

+350
-7
lines changed

7 files changed

+350
-7
lines changed

backend/config/settings.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -375,12 +375,6 @@
375375
CACHE_MIDDLEWARE_SECONDS = 600
376376
CACHE_MIDDLEWARE_KEY_PREFIX = 'atonixdev'
377377

378-
# HTTP Cache Middleware
379-
MIDDLEWARE += [
380-
'django.middleware.cache.UpdateCacheMiddleware',
381-
'django.middleware.cache.FetchFromCacheMiddleware',
382-
]
383-
384378
# Set default caching headers for responses
385379
DEFAULT_CACHE_HEADERS = {
386380
'public': True,

backend/iot_lab/agent_views.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
from django.db import transaction
44
from django.utils import timezone
5+
from django.conf import settings
6+
from django.db import connections
57
from rest_framework.permissions import AllowAny
68
from rest_framework.response import Response
79
from rest_framework.views import APIView
810

911
from .agent_auth import authenticate_agent
10-
from .models import Device, DeviceCommand, DeviceCommandLog
12+
from .models import Device, DeviceCommand, DeviceCommandLog, TelemetryRecord
1113
from .realtime import publish_command_log, publish_command_status, publish_device_update, publish_network_update
1214
from .automation_engine import run_jobs_for_event
1315

@@ -65,13 +67,42 @@ def get(self, request):
6567
auth = authenticate_agent(request)
6668
device = auth.device
6769

70+
debug_enabled = bool(settings.DEBUG) and (str(request.query_params.get('debug') or '').strip() == '1')
71+
6872
cmd = (
6973
DeviceCommand.objects.filter(device=device, status=DeviceCommand.Status.QUEUED)
7074
.order_by('queued_at', 'id')
7175
.first()
7276
)
7377

7478
if not cmd:
79+
if debug_enabled:
80+
try:
81+
db_name = connections['default'].settings_dict.get('NAME')
82+
except Exception:
83+
db_name = None
84+
85+
try:
86+
total_for_device = DeviceCommand.objects.filter(device=device).count()
87+
queued_for_device = DeviceCommand.objects.filter(
88+
device=device, status=DeviceCommand.Status.QUEUED
89+
).count()
90+
except Exception:
91+
total_for_device = None
92+
queued_for_device = None
93+
94+
return Response(
95+
{
96+
'command': None,
97+
'debug': {
98+
'device_id': device.id,
99+
'db_name': db_name,
100+
'total_commands_for_device': total_for_device,
101+
'queued_commands_for_device': queued_for_device,
102+
},
103+
}
104+
)
105+
75106
return Response({'command': None})
76107

77108
cmd.status = DeviceCommand.Status.DELIVERED
@@ -217,3 +248,73 @@ def post(self, request, command_id: int):
217248
publish_network_update({'reason': 'command_finished', 'device_id': cmd.device_id, 'command_id': cmd.id})
218249

219250
return Response({'detail': 'finished', 'id': cmd.id, 'status': cmd.status})
251+
252+
253+
class AgentTelemetryIngestView(APIView):
254+
"""Allow an edge agent to ingest telemetry using device token auth.
255+
256+
Payload:
257+
- timestamp: optional ISO string
258+
- metrics: dict
259+
- raw_text: optional string
260+
"""
261+
262+
permission_classes = [AllowAny]
263+
264+
def post(self, request):
265+
auth = authenticate_agent(request)
266+
device = auth.device
267+
268+
ts_raw = request.data.get('timestamp')
269+
metrics = request.data.get('metrics') or {}
270+
raw_text = request.data.get('raw_text') or ''
271+
272+
# Parse timestamp if provided; otherwise use now.
273+
timestamp = timezone.now()
274+
if ts_raw:
275+
try:
276+
# Django parses ISO 8601 with fromisoformat for basic cases.
277+
timestamp = timezone.datetime.fromisoformat(str(ts_raw).replace('Z', '+00:00'))
278+
if timezone.is_naive(timestamp):
279+
timestamp = timezone.make_aware(timestamp, timezone=timezone.utc)
280+
except Exception:
281+
timestamp = timezone.now()
282+
283+
rec = TelemetryRecord.objects.create(
284+
device=device,
285+
timestamp=timestamp,
286+
metrics=metrics if isinstance(metrics, dict) else {},
287+
raw_text=str(raw_text)[:20000],
288+
created_by=None,
289+
)
290+
291+
# Mark device online.
292+
Device.objects.filter(id=device.id).update(last_seen_at=timestamp, status=Device.Status.ONLINE)
293+
294+
# Fire any telemetry-driven automations.
295+
try:
296+
run_jobs_for_event(event_type='telemetry', telemetry=rec, actor=None)
297+
except Exception:
298+
pass
299+
300+
# Publish to WS dashboards.
301+
try:
302+
from .realtime import publish_telemetry_record
303+
304+
publish_telemetry_record(
305+
rec.device_id,
306+
{
307+
'id': rec.id,
308+
'device': rec.device_id,
309+
'device_name': getattr(device, 'name', None),
310+
'timestamp': rec.timestamp.isoformat() if rec.timestamp else None,
311+
'metrics': rec.metrics,
312+
'raw_text': rec.raw_text,
313+
'created_at': rec.created_at.isoformat() if rec.created_at else None,
314+
},
315+
)
316+
except Exception:
317+
pass
318+
319+
publish_network_update({'reason': 'agent_telemetry', 'device_id': device.id, 'telemetry_id': rec.id})
320+
return Response({'detail': 'ok', 'device_id': device.id, 'telemetry_id': rec.id})
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from __future__ import annotations
2+
3+
import secrets
4+
5+
from django.contrib.auth import get_user_model
6+
from django.contrib.auth.hashers import make_password
7+
from django.core.management.base import BaseCommand
8+
9+
from iot_lab.models import Device, DeviceToken
10+
11+
12+
class Command(BaseCommand):
13+
help = "Create an IoT device and mint a device token (printed once)."
14+
15+
def add_arguments(self, parser):
16+
parser.add_argument("--name", required=True)
17+
parser.add_argument("--device-type", default="raspberry_pi")
18+
parser.add_argument("--location", default="")
19+
parser.add_argument("--label", default="agent")
20+
parser.add_argument("--created-by", default="")
21+
22+
def handle(self, *args, **options):
23+
name = options["name"].strip()
24+
device_type = (options["device_type"] or "").strip()
25+
location = (options["location"] or "").strip()
26+
label = (options["label"] or "").strip()
27+
created_by_username = (options["created_by"] or "").strip()
28+
29+
created_by = None
30+
if created_by_username:
31+
User = get_user_model()
32+
created_by = User.objects.filter(username=created_by_username).first()
33+
if not created_by:
34+
self.stderr.write(f"User not found: {created_by_username}")
35+
36+
device = Device.objects.create(
37+
name=name,
38+
device_type=device_type,
39+
location=location,
40+
status=Device.Status.UNKNOWN,
41+
is_active=True,
42+
created_by=created_by,
43+
)
44+
45+
plain = secrets.token_urlsafe(32)
46+
token = DeviceToken.objects.create(
47+
device=device,
48+
label=label,
49+
token_hash=make_password(plain),
50+
created_by=created_by,
51+
)
52+
53+
self.stdout.write("Created device:")
54+
self.stdout.write(f" id: {device.id}")
55+
self.stdout.write(f" name: {device.name}")
56+
self.stdout.write(f" type: {device.device_type}")
57+
self.stdout.write(f" location: {device.location}")
58+
self.stdout.write("\nDevice token (store securely; shown once):")
59+
self.stdout.write(f" device_id: {device.id}")
60+
self.stdout.write(f" token_id: {token.id}")
61+
self.stdout.write(f" token: {plain}")
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from __future__ import annotations
2+
3+
import secrets
4+
5+
from django.contrib.auth import get_user_model
6+
from django.contrib.auth.hashers import make_password
7+
from django.core.management.base import BaseCommand
8+
9+
from iot_lab.models import Device, DeviceToken
10+
11+
12+
class Command(BaseCommand):
13+
help = "Mint a new device token for an existing device (printed once)."
14+
15+
def add_arguments(self, parser):
16+
parser.add_argument("device_id", type=int)
17+
parser.add_argument("--label", default="agent")
18+
parser.add_argument("--created-by", default="")
19+
20+
def handle(self, *args, **options):
21+
device_id = int(options["device_id"])
22+
label = (options["label"] or "").strip()
23+
created_by_username = (options["created_by"] or "").strip()
24+
25+
device = Device.objects.filter(id=device_id).first()
26+
if not device:
27+
self.stderr.write("Device not found")
28+
return
29+
30+
created_by = None
31+
if created_by_username:
32+
User = get_user_model()
33+
created_by = User.objects.filter(username=created_by_username).first()
34+
if not created_by:
35+
self.stderr.write(f"User not found: {created_by_username}")
36+
37+
plain = secrets.token_urlsafe(32)
38+
token = DeviceToken.objects.create(
39+
device=device,
40+
label=label,
41+
token_hash=make_password(plain),
42+
created_by=created_by,
43+
)
44+
45+
self.stdout.write("Minted device token (store securely; shown once):")
46+
self.stdout.write(f" device_id: {device.id}")
47+
self.stdout.write(f" token_id: {token.id}")
48+
self.stdout.write(f" token: {plain}")

backend/iot_lab/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from .agent_views import (
66
AgentHeartbeatView,
7+
AgentTelemetryIngestView,
78
AgentNextCommandView,
89
AgentCommandStartView,
910
AgentCommandLogView,
@@ -33,6 +34,7 @@
3334
urlpatterns = [
3435
*router.urls,
3536
path('agent/heartbeat/', AgentHeartbeatView.as_view(), name='iot-agent-heartbeat'),
37+
path('agent/telemetry/', AgentTelemetryIngestView.as_view(), name='iot-agent-telemetry'),
3638
path('agent/next-command/', AgentNextCommandView.as_view(), name='iot-agent-next-command'),
3739
path('agent/commands/<int:command_id>/start/', AgentCommandStartView.as_view(), name='iot-agent-command-start'),
3840
path('agent/commands/<int:command_id>/log/', AgentCommandLogView.as_view(), name='iot-agent-command-log'),

0 commit comments

Comments
 (0)