diff --git a/app/devices/migrations/0017_modelcounter_device_auto_number.py b/app/devices/migrations/0017_modelcounter_device_auto_number.py
new file mode 100644
index 0000000..34fe2ee
--- /dev/null
+++ b/app/devices/migrations/0017_modelcounter_device_auto_number.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.1.5 on 2025-01-30 10:12
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('devices', '0016_alter_device_current_campaign_and_more'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ModelCounter',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('model', models.CharField(max_length=255, unique=True)),
+ ('last_auto_number', models.IntegerField(default=0)),
+ ],
+ ),
+ migrations.AddField(
+ model_name='device',
+ name='auto_number',
+ field=models.IntegerField(blank=True, null=True),
+ ),
+ ]
diff --git a/app/devices/migrations/0018_remove_modelcounter_id_alter_modelcounter_model.py b/app/devices/migrations/0018_remove_modelcounter_id_alter_modelcounter_model.py
new file mode 100644
index 0000000..5197d24
--- /dev/null
+++ b/app/devices/migrations/0018_remove_modelcounter_id_alter_modelcounter_model.py
@@ -0,0 +1,22 @@
+# Generated by Django 5.1.5 on 2025-01-30 10:22
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('devices', '0017_modelcounter_device_auto_number'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='modelcounter',
+ name='id',
+ ),
+ migrations.AlterField(
+ model_name='modelcounter',
+ name='model',
+ field=models.CharField(max_length=255, primary_key=True, serialize=False),
+ ),
+ ]
diff --git a/app/devices/migrations/0019_alter_device_model_alter_modelcounter_model.py b/app/devices/migrations/0019_alter_device_model_alter_modelcounter_model.py
new file mode 100644
index 0000000..05730d4
--- /dev/null
+++ b/app/devices/migrations/0019_alter_device_model_alter_modelcounter_model.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.1.5 on 2025-01-30 10:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('devices', '0018_remove_modelcounter_id_alter_modelcounter_model'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='device',
+ name='model',
+ field=models.IntegerField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='modelcounter',
+ name='model',
+ field=models.IntegerField(primary_key=True, serialize=False),
+ ),
+ ]
diff --git a/app/devices/models.py b/app/devices/models.py
index ff10a31..6c998fa 100644
--- a/app/devices/models.py
+++ b/app/devices/models.py
@@ -1,4 +1,4 @@
-from django.db import models
+from django.db import models, transaction
from django.utils import timezone
from organizations.models import Organization
from campaign.models import Room
@@ -7,6 +7,8 @@
from auditlog.registry import auditlog
from auditlog.models import AuditlogHistoryField
+from main.enums import LdProduct
+
class Device(models.Model):
"""
@@ -14,23 +16,66 @@ class Device(models.Model):
"""
id = models.CharField(max_length=255, primary_key=True)
device_name = models.CharField(max_length=255, blank=True, null=True)
- model = models.CharField(max_length=255, blank=True, null=True)
+ model = models.IntegerField(null=True, blank=True)
firmware = models.CharField(max_length=255, blank=True)
btmac_address = models.CharField(max_length=12, null=True, blank=True)
last_update = models.DateTimeField(null=True, blank=True)
notes = models.TextField(null=True, blank=True)
api_key = models.CharField(max_length=64, null=True)
+ auto_number = models.IntegerField(null=True, blank=True)
current_room = models.ForeignKey(Room, related_name='current_devices', null=True, on_delete=models.SET_NULL, blank=True)
current_organization = models.ForeignKey(Organization, related_name='current_devices', null=True, on_delete=models.SET_NULL, blank=True)
current_user = models.ForeignKey(CustomUser, null=True, related_name='current_devices', on_delete=models.SET_NULL, blank=True)
current_campaign = models.ForeignKey(Campaign, null=True, related_name='current_devices', on_delete=models.SET_NULL, blank=True)
- history = AuditlogHistoryField()
+ history = AuditlogHistoryField(pk_indexable=False)
+ def save(self, *args, **kwargs):
+ # if the model id is not set or the auto_number is already set we don't
+ # need to update the auto_number
+ if self.model is None:
+ super().save(*args, **kwargs)
+ return
+
+ if self.auto_number:
+ # assign name to update existing devices
+ # TODO could be removed
+ self.device_name = f'{self.get_model_name()} {self.auto_number:04d}'
+ super().save(*args, **kwargs)
+ return
+
+ # update auto_number for the first time
+ with transaction.atomic():
+ counter, _ = ModelCounter.objects.get_or_create(model=self.model)
+ counter.last_auto_number += 1
+ counter.save()
+
+ self.auto_number = counter.last_auto_number
+ '''
+ assigns a unique name for this device in this format: "{model name}{auto_number}"
+ for example "Air Cube 0001"
+ '''
+ self.device_name = f'{self.get_model_name()} {self.auto_number:04d}'
+
+ super().save(*args, **kwargs)
+
+ def get_ble_id(self):
+ # cuts of the 3 last characters that are use for board identification
+ return self.id[:-3]
+
+ def get_model_name(self):
+ '''returns the corresponding LdProduct name'''
+ return LdProduct._names.get(self.model, 'Unknown Model')
+
def __str__(self):
return self.id or "Undefined Device" # Added fallback for undefined IDs
-
+
+
+class ModelCounter(models.Model):
+ model = models.IntegerField(primary_key=True)
+ last_auto_number = models.IntegerField(default=0)
+
class Sensor(models.Model):
"""
diff --git a/app/devices/views.py b/app/devices/views.py
index d3e7781..18b7222 100644
--- a/app/devices/views.py
+++ b/app/devices/views.py
@@ -12,8 +12,11 @@
from django.db import transaction
from .models import Device, DeviceStatus, DeviceLogs, Measurement
+from accounts.models import CustomUser
from .forms import DeviceForm, DeviceNotesForm
from main.enums import SensorModel, Dimension
+from organizations.models import Organization
+from campaign.models import Room
class DeviceListView(LoginRequiredMixin, UserPassesTestMixin, ListView):
model = Device
@@ -64,6 +67,45 @@ def get_context_data(self, **kwargs):
if status.battery_soc is not None and status.battery_voltage is not None
]
+ # query changes
+ organization_changes = device.history.filter(changes__icontains = '"current_organization"').all().order_by('-timestamp')
+ room_changes = device.history.filter(changes__icontains = '"current_room"').all().order_by('-timestamp')
+ user_changes = device.history.filter(changes__icontains = '"current_user"').all().order_by('-timestamp')
+
+ organization_change_log = []
+ room_change_log = []
+ user_change_log = []
+
+ # prepare changes
+ for h in organization_changes:
+ prev = h.changes['current_organization'][0]
+ next = h.changes['current_organization'][1]
+ organization_change_log.append({
+ 'timestamp': h.timestamp,
+ 'prev': None if prev == 'None' else Organization.objects.filter(id=prev).first(),
+ 'next': None if next == 'None' else Organization.objects.filter(id=next).first(),
+ })
+ for h in room_changes:
+ prev = h.changes['current_room'][0]
+ next = h.changes['current_room'][1]
+ room_change_log.append({
+ 'timestamp': h.timestamp,
+ 'prev': None if prev == 'None' else Room.objects.filter(id=prev).first(),
+ 'next': None if next == 'None' else Room.objects.filter(id=next).first(),
+ })
+ for h in user_changes:
+ prev = h.changes['current_user'][0]
+ next = h.changes['current_user'][1]
+ user_change_log.append({
+ 'timestamp': h.timestamp,
+ 'prev': None if prev == 'None' else CustomUser.objects.filter(id=prev).first(),
+ 'next': None if next == 'None' else CustomUser.objects.filter(id=next).first(),
+ })
+
+ context['organization_change_log'] = organization_change_log
+ context['room_change_log'] = room_change_log
+ context['user_change_log'] = user_change_log
+
# Serialize data to JSON format
context['battery_times'] = json.dumps(battery_times, cls=DjangoJSONEncoder)
context['battery_charges'] = json.dumps(battery_charges, cls=DjangoJSONEncoder)
@@ -101,7 +143,6 @@ def get_context_data(self, **kwargs):
sensors = defaultdict(list)
# add available sensors
for measurement in Measurement.objects.filter(device=device, time_measured=device.last_update).all():
- print(measurement.sensor_model)
for value in measurement.values.all():
sensors[SensorModel.get_sensor_name(measurement.sensor_model)].append(Dimension.get_name(value.dimension))
diff --git a/app/main/util.py b/app/main/util.py
index 14d2f51..8356132 100644
--- a/app/main/util.py
+++ b/app/main/util.py
@@ -36,7 +36,6 @@ def get_or_create_station(station_info: dict):
id = station_info['device']
)
if created:
- station.device_name = station_info['device']
station.model = station_info['model']
station.firmware = station_info['firmware']
station.api_key = station_info['apikey']
diff --git a/app/templates/devices/detail.html b/app/templates/devices/detail.html
index d974f95..855e51e 100644
--- a/app/templates/devices/detail.html
+++ b/app/templates/devices/detail.html
@@ -8,6 +8,7 @@
{% block styles %}
+