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 %} +