diff --git a/apps/device/handlers.py b/apps/device/handlers.py index f8d85df..cafa3cb 100644 --- a/apps/device/handlers.py +++ b/apps/device/handlers.py @@ -2,7 +2,6 @@ from django.db import transaction from django_tenants.utils import schema_context -from apps.device_model.services import create_default_device_models from apps.network_server.services import create_network_servers @@ -11,4 +10,3 @@ class NewOrganizationHandler(NewOrganizationHandlerBase): def handle(self): with schema_context(self._organization.slug_name): create_network_servers() - create_default_device_models() diff --git a/apps/device/migrations/0001_initial.py b/apps/device/migrations/0001_initial.py index 0739791..74d3a27 100644 --- a/apps/device/migrations/0001_initial.py +++ b/apps/device/migrations/0001_initial.py @@ -9,7 +9,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("device_model", "0001_initial"), ("space", "0001_initial"), ] @@ -29,12 +28,8 @@ class Migration(migrations.Migration): ), ), ( - "device_model", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="devices", - to="device_model.devicemodel", - ), + "device_model_id", + models.CharField(blank=True, null=True), ), ], options={ diff --git a/apps/device/migrations/0013_alter_device_model_to_charfield.py b/apps/device/migrations/0013_alter_device_model_to_charfield.py new file mode 100644 index 0000000..62be120 --- /dev/null +++ b/apps/device/migrations/0013_alter_device_model_to_charfield.py @@ -0,0 +1,35 @@ +# Generated by Django 5.0.6 + +from django.db import migrations, models + + +def copy_device_model_id_to_device_model(apps, schema_editor): + schema_editor.execute( + """ + UPDATE device_device + SET device_model = device_model_id + WHERE device_model_id IS NOT NULL + """ + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("device", "0012_delete_devicetransformeddata"), + ] + + operations = [ + migrations.AddField( + model_name="device", + name="device_model", + field=models.UUIDField(null=True, blank=True), + ), + migrations.RunPython( + copy_device_model_id_to_device_model, + reverse_code=migrations.RunPython.noop, + ), + migrations.RemoveField( + model_name="device", + name="device_model_id", + ), + ] diff --git a/apps/device/models.py b/apps/device/models.py index 398c891..5dc09e9 100644 --- a/apps/device/models.py +++ b/apps/device/models.py @@ -4,7 +4,6 @@ from django.db import models from apps.device.constants import DeviceStatus -from apps.device_model.models import DeviceModel from apps.network_server.models import NetworkServer @@ -17,9 +16,7 @@ class Device(BaseModel): blank=True, null=True, ) - device_model = models.ForeignKey( - DeviceModel, related_name="devices", on_delete=models.CASCADE - ) + device_model = models.UUIDField(null=True, blank=True) status = models.CharField( choices=DeviceStatus.choices, default=DeviceStatus.IN_INVENTORY ) diff --git a/apps/device/serializers.py b/apps/device/serializers.py index baa131c..75fed92 100644 --- a/apps/device/serializers.py +++ b/apps/device/serializers.py @@ -3,12 +3,12 @@ from common.utils.custom_fields import HexCharField from common.utils.telemetry_client import TelemetryServiceClient +from common.utils.tranformer_client import TranformerServiceClient from django.core.cache import cache from django.db import transaction from rest_framework import serializers from apps.device.models import Device, LorawanDevice, SpaceDevice, Trip -from apps.device_model.serializers import DeviceModelSerializer from apps.network_server.serializers import NetworkServerSerializer logger = logging.getLogger(__name__) @@ -53,17 +53,15 @@ def create(self, validated_data): class FormatDeviceSerializer(serializers.ModelSerializer): device_id = serializers.UUIDField(read_only=True, source="lorawan_device.id") - device_profile = DeviceModelSerializer(read_only=True, source="device_model") space_slug = serializers.CharField() class Meta: model = Device - fields = ["id", "device_profile", "device_id", "space_slug", "is_published"] + fields = ["id", "device_id", "device_model", "space_slug", "is_published"] class DeviceSerializer(serializers.ModelSerializer): lorawan_device = LorawanDeviceSerializer(many=False, required=False) - device_profile = DeviceModelSerializer(read_only=True, source="device_model") class Meta: model = Device @@ -71,13 +69,27 @@ class Meta: "id", "network_server", "device_model", - "device_profile", "status", "lorawan_device", "is_published", ] list_serializer_class = MultiDeviceSerializer + def to_representation(self, instance): + data = super().to_representation(instance) + device_profile = None + if instance.device_model: + try: + client = TranformerServiceClient() + device_profile = client.get_device_model(str(instance.device_model)) + except Exception as e: + logger.error( + f"Failed to fetch device model for {instance.id}: {str(e)}", + exc_info=True, + ) + data["device_profile"] = device_profile + return data + def create(self, validated_data): lorawan_data = validated_data.pop("lorawan_device", None) try: @@ -143,7 +155,6 @@ def update(self, instance, validated_data): class GetDeviceSerializer(DeviceSerializer): network_server = NetworkServerSerializer(read_only=True) - device_model = DeviceModelSerializer(read_only=True) class Meta(DeviceSerializer.Meta): model = Device diff --git a/apps/device/services/filter_processor.py b/apps/device/services/filter_processor.py index a08a9ae..2c8c953 100644 --- a/apps/device/services/filter_processor.py +++ b/apps/device/services/filter_processor.py @@ -1,12 +1,13 @@ import logging from typing import List +from common.utils.haversine_distance import haversine_distance + from apps.device.constants import ( DEFAULT_MAX_SPEED_KMH, DEFAULT_MIN_POINT_DISTANCE, LocationPoint, ) -from apps.utils.haversine_distance import haversine_distance logger = logging.getLogger(__name__) diff --git a/apps/device/services/trip_analyzer.py b/apps/device/services/trip_analyzer.py index 759ecf6..c891b7e 100644 --- a/apps/device/services/trip_analyzer.py +++ b/apps/device/services/trip_analyzer.py @@ -8,13 +8,13 @@ from typing import List, Tuple import pytz +from common.utils.haversine_distance import haversine_distance from common.utils.telemetry_client import LocationPoint, TelemetryServiceClient from django.conf import settings from django.db import transaction from apps.device.models import SpaceDevice, Trip from apps.device.services.filter_processor import FilterProcessor -from apps.utils.haversine_distance import haversine_distance logger = logging.getLogger(__name__) diff --git a/apps/device/views.py b/apps/device/views.py index c0386f0..3cb39d9 100644 --- a/apps/device/views.py +++ b/apps/device/views.py @@ -72,7 +72,7 @@ class ListCreateSpaceDeviceViewSet(generics.ListCreateAPIView): "id", "device__lorawan_device__dev_eui", "device__id", - "device__device_model__name", + "device__device_model", ] def get_serializer_class(self): @@ -283,9 +283,9 @@ def list(self, request, *args, **kwargs): class DeviceLookupView(UseTenantFromRequestMixin, generics.GenericAPIView): serializer_class = FormatDeviceSerializer lookup_field = "lorawan_device__dev_eui" - queryset = Device.objects.select_related( - "device_model", "device_model__manufacture", "lorawan_device" - ).prefetch_related("space_devices") + queryset = Device.objects.select_related("lorawan_device").prefetch_related( + "space_devices" + ) def get_queryset(self): qs = super().get_queryset() diff --git a/apps/device_model/__init__.py b/apps/device_model/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/device_model/apps.py b/apps/device_model/apps.py deleted file mode 100644 index 785e775..0000000 --- a/apps/device_model/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class DeviceModelConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "apps.device_model" diff --git a/apps/device_model/constants.py b/apps/device_model/constants.py deleted file mode 100644 index 3cb9545..0000000 --- a/apps/device_model/constants.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.db import models - - -class KeyFeature(models.TextChoices): - WATER_DEPTH_TYPE = "water_depth" - LOCATION_TYPE = "location" diff --git a/apps/device_model/migrations/0001_initial.py b/apps/device_model/migrations/0001_initial.py deleted file mode 100644 index 79695ad..0000000 --- a/apps/device_model/migrations/0001_initial.py +++ /dev/null @@ -1,83 +0,0 @@ -# Generated by Django 5.0.6 on 2024-11-20 07:55 - -import django.db.models.deletion -import uuid -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="DeviceManufacture", - fields=[ - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ("name", models.CharField(max_length=255)), - ("location", models.CharField(blank=True, max_length=255, null=True)), - ("description", models.TextField(blank=True, null=True)), - ("portal_url", models.URLField(blank=True, null=True)), - ("national", models.CharField(blank=True, max_length=255, null=True)), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="DeviceModel", - fields=[ - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ("name", models.CharField(max_length=255)), - ("alias", models.CharField(blank=True, max_length=255, null=True)), - ("image_url", models.URLField(blank=True, null=True)), - ( - "device_type", - models.CharField( - choices=[ - ("ttn_gateway", "TTN Gateway"), - ("ttn", "TTN"), - ("chirpstack", "Chirpstack"), - ("mqtt", "MQTT"), - ], - max_length=50, - ), - ), - ("default_config", models.JSONField(blank=True, null=True)), - ( - "manufacture", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="device_models", - to="device_model.devicemanufacture", - ), - ), - ], - options={ - "indexes": [ - models.Index(fields=["alias"], name="device_mode_alias_ed37e0_idx") - ], - }, - ), - ] diff --git a/apps/device_model/migrations/0002_remove_devicemodel_device_type.py b/apps/device_model/migrations/0002_remove_devicemodel_device_type.py deleted file mode 100644 index a3dc051..0000000 --- a/apps/device_model/migrations/0002_remove_devicemodel_device_type.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.0.6 on 2025-05-20 07:59 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("device_model", "0001_initial"), - ] - - operations = [ - migrations.RemoveField( - model_name="devicemodel", - name="device_type", - ), - ] diff --git a/apps/device_model/migrations/0003_devicemodel_device_type.py b/apps/device_model/migrations/0003_devicemodel_device_type.py deleted file mode 100644 index 49c1820..0000000 --- a/apps/device_model/migrations/0003_devicemodel_device_type.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.0.6 on 2025-05-22 09:03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("device_model", "0002_remove_devicemodel_device_type"), - ] - - operations = [ - migrations.AddField( - model_name="devicemodel", - name="device_type", - field=models.CharField( - blank=True, choices=[("lorawan", "LoRaWAN")], max_length=255, null=True - ), - ), - ] diff --git a/apps/device_model/migrations/0004_remove_devicemodel_device_mode_alias_ed37e0_idx_and_more.py b/apps/device_model/migrations/0004_remove_devicemodel_device_mode_alias_ed37e0_idx_and_more.py deleted file mode 100644 index 9c39c42..0000000 --- a/apps/device_model/migrations/0004_remove_devicemodel_device_mode_alias_ed37e0_idx_and_more.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 5.0.6 on 2025-09-25 06:38 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("device_model", "0003_devicemodel_device_type"), - ] - - operations = [ - migrations.RemoveIndex( - model_name="devicemodel", - name="device_mode_alias_ed37e0_idx", - ), - migrations.RemoveField( - model_name="devicemodel", - name="alias", - ), - migrations.AlterField( - model_name="devicemodel", - name="manufacture", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="device_models", - to="device_model.devicemanufacture", - ), - ), - migrations.AddIndex( - model_name="devicemodel", - index=models.Index(fields=["name"], name="device_mode_name_d69fc8_idx"), - ), - ] diff --git a/apps/device_model/migrations/0005_devicemodel_key_feature.py b/apps/device_model/migrations/0005_devicemodel_key_feature.py deleted file mode 100644 index 940d94c..0000000 --- a/apps/device_model/migrations/0005_devicemodel_key_feature.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 5.0.6 on 2025-12-13 11:21 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ( - "device_model", - "0004_remove_devicemodel_device_mode_alias_ed37e0_idx_and_more", - ), - ] - - operations = [ - migrations.AddField( - model_name="devicemodel", - name="key_feature", - field=models.CharField( - blank=True, - choices=[ - ("water_level_sensor", "Water Level Sensor"), - ("multi_sensor_tracker", "Multi Sensor Tracker"), - ], - default="multi_sensor_tracker", - max_length=255, - null=True, - ), - ), - ] diff --git a/apps/device_model/migrations/0006_alter_devicemodel_key_feature.py b/apps/device_model/migrations/0006_alter_devicemodel_key_feature.py deleted file mode 100644 index 1008759..0000000 --- a/apps/device_model/migrations/0006_alter_devicemodel_key_feature.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.0.6 on 2025-12-13 14:39 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("device_model", "0005_devicemodel_key_feature"), - ] - - operations = [ - migrations.AlterField( - model_name="devicemodel", - name="key_feature", - field=models.CharField( - blank=True, - choices=[ - ("water_depth", "Water Depth Type"), - ("location", "Location Type"), - ], - default="location", - max_length=255, - null=True, - ), - ), - ] diff --git a/apps/device_model/migrations/__init__.py b/apps/device_model/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/device_model/models.py b/apps/device_model/models.py deleted file mode 100644 index 1b768e1..0000000 --- a/apps/device_model/models.py +++ /dev/null @@ -1,46 +0,0 @@ -import uuid - -from common.apps.space.models import BaseModel -from django.db import models - -from apps.device_model.constants import KeyFeature - - -class DeviceManufacture(BaseModel): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - name = models.CharField(max_length=255) - location = models.CharField(max_length=255, blank=True, null=True) - description = models.TextField(blank=True, null=True) - portal_url = models.URLField(blank=True, null=True) - national = models.CharField(max_length=255, blank=True, null=True) - - -class DeviceModel(BaseModel): - DEVICE_TYPE = (("lorawan", "LoRaWAN"),) - - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - name = models.CharField(max_length=255) - image_url = models.URLField(blank=True, null=True) - device_type = models.CharField( - max_length=255, choices=DEVICE_TYPE, blank=True, null=True - ) - manufacture = models.ForeignKey( - DeviceManufacture, - related_name="device_models", - on_delete=models.CASCADE, - blank=True, - null=True, - ) - key_feature = models.CharField( - max_length=255, - blank=True, - null=True, - choices=KeyFeature.choices, - default=KeyFeature.LOCATION_TYPE, - ) - default_config = models.JSONField(blank=True, null=True) - - class Meta: - indexes = [ - models.Index(fields=["name"]), - ] diff --git a/apps/device_model/serializers.py b/apps/device_model/serializers.py deleted file mode 100644 index 86bfae1..0000000 --- a/apps/device_model/serializers.py +++ /dev/null @@ -1,19 +0,0 @@ -from rest_framework import serializers - -from apps.device_model.models import DeviceManufacture, DeviceModel - - -class DeviceManufactureSerializer(serializers.ModelSerializer): - class Meta: - model = DeviceManufacture - fields = "__all__" - extra_kwargs = {"id": {"read_only": True}} - - -class DeviceModelSerializer(serializers.ModelSerializer): - manufacture = serializers.CharField(source="manufacture.name", read_only=True) - - class Meta: - model = DeviceModel - fields = "__all__" - extra_kwargs = {"id": {"read_only": True}} diff --git a/apps/device_model/services.py b/apps/device_model/services.py deleted file mode 100644 index d19ad63..0000000 --- a/apps/device_model/services.py +++ /dev/null @@ -1,42 +0,0 @@ -from apps.device_model.constants import KeyFeature -from apps.device_model.models import DeviceModel - -default_device_models = [ - { - "name": "lorawan", - "device_type": "RAK2270", - "key_feature": KeyFeature.LOCATION_TYPE, - }, - { - "name": "lorawan", - "device_type": "RAK4630", - "key_feature": KeyFeature.LOCATION_TYPE, - }, - { - "name": "lorawan", - "device_type": "WLBV1", - "key_feature": KeyFeature.WATER_DEPTH_TYPE, - }, - { - "name": "lorawan", - "device_type": "SENSECAP_T1000", - "key_feature": KeyFeature.LOCATION_TYPE, - }, - { - "name": "lorawan", - "device_type": "ABEEWAY_INDUSTRIAL_TRACKER", - "key_feature": KeyFeature.LOCATION_TYPE, - }, -] - - -def create_default_device_models(): - list_data = [ - DeviceModel( - name=default_device_model.get("name"), - device_type=default_device_model.get("device_type"), - key_feature=default_device_model.get("key_feature"), - ) - for default_device_model in default_device_models - ] - DeviceModel.objects.bulk_create(list_data) diff --git a/apps/device_model/urls.py b/apps/device_model/urls.py deleted file mode 100644 index d740c08..0000000 --- a/apps/device_model/urls.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.urls import include, path -from rest_framework.routers import DefaultRouter - -from apps.device_model.views import DeviceManufactureViewSet, DeviceModelViewSet - -app_name = "device_model" - -router = DefaultRouter() -router.register("manufacturers", DeviceManufactureViewSet) -router.register("device-models", DeviceModelViewSet) - -urlpatterns = [ - path("", include(router.urls)), -] diff --git a/apps/device_model/views.py b/apps/device_model/views.py deleted file mode 100644 index 62daf21..0000000 --- a/apps/device_model/views.py +++ /dev/null @@ -1,28 +0,0 @@ -from common.pagination.base_pagination import BasePagination -from common.utils.switch_tenant import UseTenantFromRequestMixin -from rest_framework import viewsets -from rest_framework.filters import OrderingFilter, SearchFilter - -from apps.device_model.models import DeviceManufacture, DeviceModel -from apps.device_model.serializers import ( - DeviceManufactureSerializer, - DeviceModelSerializer, -) - - -class DeviceManufactureViewSet(UseTenantFromRequestMixin, viewsets.ModelViewSet): - queryset = DeviceManufacture.objects.all() - serializer_class = DeviceManufactureSerializer - pagination_class = BasePagination - filter_backends = [OrderingFilter, SearchFilter] - ordering_fields = ["name"] - search_fields = ["name"] - - -class DeviceModelViewSet(UseTenantFromRequestMixin, viewsets.ModelViewSet): - queryset = DeviceModel.objects.all() - serializer_class = DeviceModelSerializer - pagination_class = BasePagination - filter_backends = [OrderingFilter, SearchFilter] - ordering_fields = ["name"] - search_fields = ["name", "device_type"] diff --git a/apps/utils/haversine_distance.py b/apps/utils/haversine_distance.py deleted file mode 100644 index c5e5893..0000000 --- a/apps/utils/haversine_distance.py +++ /dev/null @@ -1,24 +0,0 @@ -from math import asin, cos, radians, sin, sqrt - - -def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float: - """ - Calculate distance between two GPS coordinates using Haversine formula. - - The Haversine formula calculates the great-circle distance between two points - on a sphere given their latitudes and longitudes. - - """ - # Radius of Earth in meters - R = 6_371_000 - - # Convert to radians - lat1_rad, lon1_rad, lat2_rad, lon2_rad = map(radians, [lat1, lon1, lat2, lon2]) - - # Haversine formula - dlat = lat2_rad - lat1_rad - dlon = lon2_rad - lon1_rad - a = sin(dlat / 2) ** 2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2) ** 2 - c = 2 * asin(sqrt(a)) - - return R * c diff --git a/device_service/settings.py b/device_service/settings.py index c9e1d89..ec6b011 100644 --- a/device_service/settings.py +++ b/device_service/settings.py @@ -56,7 +56,6 @@ TENANT_APPS = [ "django.contrib.auth", "common.apps.space", - "apps.device_model", "apps.device", "apps.device_connector", "apps.network_server", @@ -176,6 +175,11 @@ # Telemetry Service Configuration TELEMETRY_SERVICE_URL = os.getenv("TELEMETRY_SERVICE_URL", "http://telemetry:8080") +# Transformer Service Configuration +TRANSFORMER_SERVICE_URL = os.getenv( + "TRANSFORMER_SERVICE_URL", "http://transformer:8080" +) + # Redis cache CACHES = { "default": { diff --git a/device_service/urls.py b/device_service/urls.py index a59d6be..accb260 100644 --- a/device_service/urls.py +++ b/device_service/urls.py @@ -41,7 +41,6 @@ name="schema-swagger-ui", ), # apis - path("api/", include("apps.device_model.urls", namespace="device_model")), path("api/", include("apps.network_server.urls", namespace="network_server")), path("api/", include("apps.device.urls", namespace="device")), ] diff --git a/device_service/urls_public.py b/device_service/urls_public.py index 8ca6fcf..46a9750 100644 --- a/device_service/urls_public.py +++ b/device_service/urls_public.py @@ -52,7 +52,6 @@ def health_check(_): ), # health path("device/api/health", health_check), - path("api/", include("apps.device_model.urls", namespace="device_model")), path("api/", include("apps.network_server.urls", namespace="network_server")), path("api/", include("apps.device.urls", namespace="device")), ]