Răsfoiți Sursa

cooling diameter

Arthur 1 săptămână în urmă
părinte
comite
8a7d275343

+ 1 - 1
docs/models/dcim/coolingoutlet.md

@@ -33,7 +33,7 @@ The physical coolant connector type (e.g. UQD, UQDB, QDC, camlock, or threaded N
 
 
 ### Diameter
 ### Diameter
 
 
-The nominal connector diameter.
+The connector diameter, expressed as a numeric value with a selectable unit (millimeters, centimeters, or inches).
 
 
 ### Cooling Port
 ### Cooling Port
 
 

+ 1 - 1
docs/models/dcim/coolingport.md

@@ -33,7 +33,7 @@ The physical coolant connector type (e.g. UQD, UQDB, QDC, camlock, or threaded N
 
 
 ### Diameter
 ### Diameter
 
 
-The nominal connector diameter.
+The connector diameter, expressed as a numeric value with a selectable unit (millimeters, centimeters, or inches).
 
 
 ### Maximum Flow
 ### Maximum Flow
 
 

+ 9 - 6
netbox/dcim/api/serializers_/device_components.py

@@ -26,6 +26,7 @@ from ipam.models import VLAN
 from netbox.api.fields import ChoiceField, ContentTypeField, RestrictedPrimaryKeyRelatedField, SerializedPKRelatedField
 from netbox.api.fields import ChoiceField, ContentTypeField, RestrictedPrimaryKeyRelatedField, SerializedPKRelatedField
 from netbox.api.gfk_fields import GFKSerializerField
 from netbox.api.gfk_fields import GFKSerializerField
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.api.serializers import NetBoxModelSerializer
+from netbox.choices import DiameterUnitChoices
 from users.api.serializers_.mixins import OwnerMixin
 from users.api.serializers_.mixins import OwnerMixin
 from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
 from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
 from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
 from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
@@ -225,8 +226,8 @@ class CoolingPortSerializer(
         required=False,
         required=False,
         allow_null=True
         allow_null=True
     )
     )
-    diameter = ChoiceField(
-        choices=CoolingDiameterChoices,
+    diameter_unit = ChoiceField(
+        choices=DiameterUnitChoices,
         allow_blank=True,
         allow_blank=True,
         required=False,
         required=False,
         allow_null=True
         allow_null=True
@@ -236,7 +237,8 @@ class CoolingPortSerializer(
         model = CoolingPort
         model = CoolingPort
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'connector_type',
             'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'connector_type',
-            'diameter', 'maximum_flow', 'heat_capacity', 'description', 'mark_connected', 'cable', 'cable_end',
+            'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity', 'description', 'mark_connected', 'cable',
+            'cable_end',
             'link_peers', 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type',
             'link_peers', 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type',
             'connected_endpoints_reachable', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
             'connected_endpoints_reachable', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
         ]
         ]
@@ -268,8 +270,8 @@ class CoolingOutletSerializer(
         required=False,
         required=False,
         allow_null=True
         allow_null=True
     )
     )
-    diameter = ChoiceField(
-        choices=CoolingDiameterChoices,
+    diameter_unit = ChoiceField(
+        choices=DiameterUnitChoices,
         allow_blank=True,
         allow_blank=True,
         required=False,
         required=False,
         allow_null=True
         allow_null=True
@@ -284,7 +286,8 @@ class CoolingOutletSerializer(
         model = CoolingOutlet
         model = CoolingOutlet
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'connector_type',
             'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'connector_type',
-            'diameter', 'color', 'cooling_port', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
+            'diameter', 'diameter_unit', 'color', 'cooling_port', 'description', 'mark_connected', 'cable', 'cable_end',
+            'link_peers',
             'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
             'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
             'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
             'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
         ]
         ]

+ 7 - 6
netbox/dcim/api/serializers_/devicetype_components.py

@@ -21,6 +21,7 @@ from dcim.models import (
 from netbox.api.fields import ChoiceField, ContentTypeField, RestrictedPrimaryKeyRelatedField
 from netbox.api.fields import ChoiceField, ContentTypeField, RestrictedPrimaryKeyRelatedField
 from netbox.api.gfk_fields import GFKSerializerField
 from netbox.api.gfk_fields import GFKSerializerField
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
+from netbox.choices import DiameterUnitChoices
 from wireless.choices import *
 from wireless.choices import *
 
 
 from .base import PortSerializer
 from .base import PortSerializer
@@ -199,8 +200,8 @@ class CoolingPortTemplateSerializer(ComponentTemplateSerializer):
         required=False,
         required=False,
         allow_null=True
         allow_null=True
     )
     )
-    diameter = ChoiceField(
-        choices=CoolingDiameterChoices,
+    diameter_unit = ChoiceField(
+        choices=DiameterUnitChoices,
         allow_blank=True,
         allow_blank=True,
         required=False,
         required=False,
         allow_null=True
         allow_null=True
@@ -210,7 +211,7 @@ class CoolingPortTemplateSerializer(ComponentTemplateSerializer):
         model = CoolingPortTemplate
         model = CoolingPortTemplate
         fields = [
         fields = [
             'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'connector_type',
             'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'connector_type',
-            'diameter', 'maximum_flow', 'heat_capacity', 'description', 'created', 'last_updated',
+            'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity', 'description', 'created', 'last_updated',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'description')
         brief_fields = ('id', 'url', 'display', 'name', 'description')
 
 
@@ -240,8 +241,8 @@ class CoolingOutletTemplateSerializer(ComponentTemplateSerializer):
         required=False,
         required=False,
         allow_null=True
         allow_null=True
     )
     )
-    diameter = ChoiceField(
-        choices=CoolingDiameterChoices,
+    diameter_unit = ChoiceField(
+        choices=DiameterUnitChoices,
         allow_blank=True,
         allow_blank=True,
         required=False,
         required=False,
         allow_null=True
         allow_null=True
@@ -256,7 +257,7 @@ class CoolingOutletTemplateSerializer(ComponentTemplateSerializer):
         model = CoolingOutletTemplate
         model = CoolingOutletTemplate
         fields = [
         fields = [
             'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'connector_type',
             'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'connector_type',
-            'diameter', 'color', 'cooling_port', 'description', 'created', 'last_updated',
+            'diameter', 'diameter_unit', 'color', 'cooling_port', 'description', 'created', 'last_updated',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'description')
         brief_fields = ('id', 'url', 'display', 'name', 'description')
 
 

+ 0 - 21
netbox/dcim/choices.py

@@ -2219,27 +2219,6 @@ class CoolingConnectorTypeChoices(ChoiceSet):
     ]
     ]
 
 
 
 
-class CoolingDiameterChoices(ChoiceSet):
-
-    DN10 = 'dn10'
-    DN15 = 'dn15'
-    DN20 = 'dn20'
-    DN25 = 'dn25'
-    DN32 = 'dn32'
-    DN40 = 'dn40'
-    DN50 = 'dn50'
-
-    CHOICES = [
-        (DN10, 'DN10'),
-        (DN15, 'DN15'),
-        (DN20, 'DN20'),
-        (DN25, 'DN25'),
-        (DN32, 'DN32'),
-        (DN40, 'DN40'),
-        (DN50, 'DN50'),
-    ]
-
-
 #
 #
 # VDC
 # VDC
 #
 #

+ 7 - 16
netbox/dcim/filtersets.py

@@ -1015,8 +1015,8 @@ class CoolingPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
     class Meta:
     class Meta:
         model = CoolingPortTemplate
         model = CoolingPortTemplate
         fields = (
         fields = (
-            'id', 'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity',
-            'description',
+            'id', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow',
+            'heat_capacity', 'description',
         )
         )
 
 
 
 
@@ -1030,7 +1030,7 @@ class CoolingOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTy
 
 
     class Meta:
     class Meta:
         model = CoolingOutletTemplate
         model = CoolingOutletTemplate
-        fields = ('id', 'name', 'label', 'type', 'connector_type', 'diameter', 'color', 'description')
+        fields = ('id', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'color', 'description')
 
 
 
 
 @register_filterset
 @register_filterset
@@ -2078,17 +2078,12 @@ class CoolingPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
         distinct=False,
         distinct=False,
         null_value=None
         null_value=None
     )
     )
-    diameter = django_filters.MultipleChoiceFilter(
-        choices=CoolingDiameterChoices,
-        distinct=False,
-        null_value=None
-    )
 
 
     class Meta:
     class Meta:
         model = CoolingPort
         model = CoolingPort
         fields = (
         fields = (
-            'id', 'name', 'label', 'maximum_flow', 'heat_capacity', 'description', 'mark_connected', 'cable_end',
-            'cable_connector',
+            'id', 'name', 'label', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity', 'description',
+            'mark_connected', 'cable_end', 'cable_connector',
         )
         )
 
 
 
 
@@ -2104,11 +2099,6 @@ class CoolingOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilter
         distinct=False,
         distinct=False,
         null_value=None
         null_value=None
     )
     )
-    diameter = django_filters.MultipleChoiceFilter(
-        choices=CoolingDiameterChoices,
-        distinct=False,
-        null_value=None
-    )
     cooling_port_id = django_filters.ModelMultipleChoiceFilter(
     cooling_port_id = django_filters.ModelMultipleChoiceFilter(
         queryset=CoolingPort.objects.all(),
         queryset=CoolingPort.objects.all(),
         distinct=False,
         distinct=False,
@@ -2118,7 +2108,8 @@ class CoolingOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilter
     class Meta:
     class Meta:
         model = CoolingOutlet
         model = CoolingOutlet
         fields = (
         fields = (
-            'id', 'name', 'label', 'description', 'color', 'mark_connected', 'cable_end', 'cable_connector',
+            'id', 'name', 'label', 'diameter', 'diameter_unit', 'description', 'color', 'mark_connected', 'cable_end',
+            'cable_connector',
         )
         )
 
 
 
 

+ 46 - 14
netbox/dcim/forms/bulk_edit.py

@@ -1346,9 +1346,13 @@ class CoolingPortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
         choices=add_blank_choice(CoolingConnectorTypeChoices),
         choices=add_blank_choice(CoolingConnectorTypeChoices),
         required=False
         required=False
     )
     )
-    diameter = forms.ChoiceField(
+    diameter = forms.DecimalField(
         label=_('Diameter'),
         label=_('Diameter'),
-        choices=add_blank_choice(CoolingDiameterChoices),
+        required=False
+    )
+    diameter_unit = forms.ChoiceField(
+        label=_('Diameter unit'),
+        choices=add_blank_choice(DiameterUnitChoices),
         required=False
         required=False
     )
     )
     maximum_flow = forms.DecimalField(
     maximum_flow = forms.DecimalField(
@@ -1366,8 +1370,16 @@ class CoolingPortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
         required=False
         required=False
     )
     )
 
 
+    fieldsets = (
+        FieldSet(
+            'label', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'maximum_flow', 'heat_capacity', 'description',
+        ),
+    )
     nullable_fields = (
     nullable_fields = (
-        'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity', 'description',
+        'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity',
+        'description',
     )
     )
 
 
 
 
@@ -1398,9 +1410,13 @@ class CoolingOutletTemplateBulkEditForm(ComponentTemplateBulkEditForm):
         choices=add_blank_choice(CoolingConnectorTypeChoices),
         choices=add_blank_choice(CoolingConnectorTypeChoices),
         required=False
         required=False
     )
     )
-    diameter = forms.ChoiceField(
+    diameter = forms.DecimalField(
         label=_('Diameter'),
         label=_('Diameter'),
-        choices=add_blank_choice(CoolingDiameterChoices),
+        required=False
+    )
+    diameter_unit = forms.ChoiceField(
+        label=_('Diameter unit'),
+        choices=add_blank_choice(DiameterUnitChoices),
         required=False
         required=False
     )
     )
     color = ColorField(
     color = ColorField(
@@ -1417,8 +1433,15 @@ class CoolingOutletTemplateBulkEditForm(ComponentTemplateBulkEditForm):
         required=False
         required=False
     )
     )
 
 
+    fieldsets = (
+        FieldSet(
+            'label', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'color', 'cooling_port', 'description',
+        ),
+    )
     nullable_fields = (
     nullable_fields = (
-        'label', 'type', 'connector_type', 'diameter', 'cooling_port', 'description',
+        'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'cooling_port', 'description',
     )
     )
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -1734,8 +1757,8 @@ class PowerOutletBulkEditForm(
 class CoolingPortBulkEditForm(
 class CoolingPortBulkEditForm(
     ComponentBulkEditForm,
     ComponentBulkEditForm,
     form_from_model(CoolingPort, [
     form_from_model(CoolingPort, [
-        'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity', 'mark_connected',
-        'description'
+        'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity',
+        'mark_connected', 'description'
     ])
     ])
 ):
 ):
     mark_connected = forms.NullBooleanField(
     mark_connected = forms.NullBooleanField(
@@ -1746,11 +1769,16 @@ class CoolingPortBulkEditForm(
 
 
     model = CoolingPort
     model = CoolingPort
     fieldsets = (
     fieldsets = (
-        FieldSet('module', 'type', 'connector_type', 'diameter', 'label', 'description', 'mark_connected'),
+        FieldSet(
+            'module', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'label', 'description', 'mark_connected',
+        ),
         FieldSet('maximum_flow', 'heat_capacity', name=_('Characteristics')),
         FieldSet('maximum_flow', 'heat_capacity', name=_('Characteristics')),
     )
     )
     nullable_fields = (
     nullable_fields = (
-        'module', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity', 'description',
+        'module', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity',
+        'description',
     )
     )
 
 
 
 
@@ -1758,7 +1786,10 @@ class CoolingOutletBulkEditForm(
     ComponentBulkEditForm,
     ComponentBulkEditForm,
     form_from_model(
     form_from_model(
         CoolingOutlet,
         CoolingOutlet,
-        ['label', 'type', 'connector_type', 'diameter', 'color', 'cooling_port', 'mark_connected', 'description']
+        [
+            'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'color', 'cooling_port', 'mark_connected',
+            'description'
+        ]
     )
     )
 ):
 ):
     mark_connected = forms.NullBooleanField(
     mark_connected = forms.NullBooleanField(
@@ -1770,12 +1801,13 @@ class CoolingOutletBulkEditForm(
     model = CoolingOutlet
     model = CoolingOutlet
     fieldsets = (
     fieldsets = (
         FieldSet(
         FieldSet(
-            'module', 'type', 'connector_type', 'diameter', 'label', 'description', 'mark_connected', 'color',
-            'cooling_port'
+            'module', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'label', 'description', 'mark_connected', 'color', 'cooling_port',
         ),
         ),
     )
     )
     nullable_fields = (
     nullable_fields = (
-        'module', 'label', 'type', 'connector_type', 'diameter', 'cooling_port', 'description',
+        'module', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'cooling_port', 'description',
     )
     )
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):

+ 12 - 12
netbox/dcim/forms/bulk_import.py

@@ -988,18 +988,18 @@ class CoolingPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
         required=False,
         required=False,
         help_text=_('Physical connector type')
         help_text=_('Physical connector type')
     )
     )
-    diameter = CSVChoiceField(
-        label=_('Diameter'),
-        choices=CoolingDiameterChoices,
+    diameter_unit = CSVChoiceField(
+        label=_('Diameter unit'),
+        choices=DiameterUnitChoices,
         required=False,
         required=False,
-        help_text=_('Nominal connector diameter')
+        help_text=_('Diameter unit')
     )
     )
 
 
     class Meta:
     class Meta:
         model = CoolingPort
         model = CoolingPort
         fields = (
         fields = (
-            'device', 'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity',
-            'mark_connected', 'description', 'owner', 'tags',
+            'device', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow',
+            'heat_capacity', 'mark_connected', 'description', 'owner', 'tags',
         )
         )
 
 
 
 
@@ -1021,11 +1021,11 @@ class CoolingOutletImportForm(OwnerCSVMixin, NetBoxModelImportForm):
         required=False,
         required=False,
         help_text=_('Physical connector type')
         help_text=_('Physical connector type')
     )
     )
-    diameter = CSVChoiceField(
-        label=_('Diameter'),
-        choices=CoolingDiameterChoices,
+    diameter_unit = CSVChoiceField(
+        label=_('Diameter unit'),
+        choices=DiameterUnitChoices,
         required=False,
         required=False,
-        help_text=_('Nominal connector diameter')
+        help_text=_('Diameter unit')
     )
     )
     cooling_port = CSVModelChoiceField(
     cooling_port = CSVModelChoiceField(
         label=_('Cooling port'),
         label=_('Cooling port'),
@@ -1038,8 +1038,8 @@ class CoolingOutletImportForm(OwnerCSVMixin, NetBoxModelImportForm):
     class Meta:
     class Meta:
         model = CoolingOutlet
         model = CoolingOutlet
         fields = (
         fields = (
-            'device', 'name', 'label', 'type', 'connector_type', 'diameter', 'color', 'mark_connected', 'cooling_port',
-            'description', 'owner', 'tags',
+            'device', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'color', 'mark_connected',
+            'cooling_port', 'description', 'owner', 'tags',
         )
         )
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):

+ 30 - 12
netbox/dcim/forms/filtersets.py

@@ -1782,7 +1782,7 @@ class CoolingPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = CoolingPort
     model = CoolingPort
     fieldsets = (
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
         FieldSet('q', 'filter_id', 'tag'),
-        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', name=_('Attributes')),
+        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', name=_('Attributes')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet(
         FieldSet(
             'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
             'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
@@ -1801,9 +1801,13 @@ class CoolingPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
         choices=CoolingConnectorTypeChoices,
         choices=CoolingConnectorTypeChoices,
         required=False
         required=False
     )
     )
-    diameter = forms.MultipleChoiceField(
+    diameter = forms.DecimalField(
         label=_('Diameter'),
         label=_('Diameter'),
-        choices=CoolingDiameterChoices,
+        required=False
+    )
+    diameter_unit = forms.ChoiceField(
+        label=_('Diameter unit'),
+        choices=add_blank_choice(DiameterUnitChoices),
         required=False
         required=False
     )
     )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
@@ -1813,7 +1817,7 @@ class CoolingPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
     model = CoolingPortTemplate
     model = CoolingPortTemplate
     fieldsets = (
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
         FieldSet('q', 'filter_id', 'tag'),
-        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', name=_('Attributes')),
+        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', name=_('Attributes')),
         FieldSet('device_type_id', 'module_type_id', name=_('Device')),
         FieldSet('device_type_id', 'module_type_id', name=_('Device')),
     )
     )
     type = forms.MultipleChoiceField(
     type = forms.MultipleChoiceField(
@@ -1826,9 +1830,13 @@ class CoolingPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
         choices=CoolingConnectorTypeChoices,
         choices=CoolingConnectorTypeChoices,
         required=False
         required=False
     )
     )
-    diameter = forms.MultipleChoiceField(
+    diameter = forms.DecimalField(
         label=_('Diameter'),
         label=_('Diameter'),
-        choices=CoolingDiameterChoices,
+        required=False
+    )
+    diameter_unit = forms.ChoiceField(
+        label=_('Diameter unit'),
+        choices=add_blank_choice(DiameterUnitChoices),
         required=False
         required=False
     )
     )
 
 
@@ -1837,7 +1845,9 @@ class CoolingOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm)
     model = CoolingOutlet
     model = CoolingOutlet
     fieldsets = (
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
         FieldSet('q', 'filter_id', 'tag'),
-        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', 'color', name=_('Attributes')),
+        FieldSet(
+            'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'color', name=_('Attributes')
+        ),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet(
         FieldSet(
             'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
             'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
@@ -1856,9 +1866,13 @@ class CoolingOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm)
         choices=CoolingConnectorTypeChoices,
         choices=CoolingConnectorTypeChoices,
         required=False
         required=False
     )
     )
-    diameter = forms.MultipleChoiceField(
+    diameter = forms.DecimalField(
         label=_('Diameter'),
         label=_('Diameter'),
-        choices=CoolingDiameterChoices,
+        required=False
+    )
+    diameter_unit = forms.ChoiceField(
+        label=_('Diameter unit'),
+        choices=add_blank_choice(DiameterUnitChoices),
         required=False
         required=False
     )
     )
     color = ColorField(
     color = ColorField(
@@ -1872,7 +1886,7 @@ class CoolingOutletTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
     model = CoolingOutletTemplate
     model = CoolingOutletTemplate
     fieldsets = (
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
         FieldSet('q', 'filter_id', 'tag'),
-        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', name=_('Attributes')),
+        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', name=_('Attributes')),
         FieldSet('device_type_id', 'module_type_id', name=_('Device')),
         FieldSet('device_type_id', 'module_type_id', name=_('Device')),
     )
     )
     type = forms.MultipleChoiceField(
     type = forms.MultipleChoiceField(
@@ -1885,9 +1899,13 @@ class CoolingOutletTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
         choices=CoolingConnectorTypeChoices,
         choices=CoolingConnectorTypeChoices,
         required=False
         required=False
     )
     )
-    diameter = forms.MultipleChoiceField(
+    diameter = forms.DecimalField(
         label=_('Diameter'),
         label=_('Diameter'),
-        choices=CoolingDiameterChoices,
+        required=False
+    )
+    diameter_unit = forms.ChoiceField(
+        label=_('Diameter unit'),
+        choices=add_blank_choice(DiameterUnitChoices),
         required=False
         required=False
     )
     )
 
 

+ 20 - 14
netbox/dcim/forms/model_forms.py

@@ -1251,15 +1251,17 @@ class CoolingPortTemplateForm(ModularComponentTemplateForm):
                 FieldSet('device_type', name=_('Device Type')),
                 FieldSet('device_type', name=_('Device Type')),
                 FieldSet('module_type', name=_('Module Type')),
                 FieldSet('module_type', name=_('Module Type')),
             ),
             ),
-            'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity', 'description',
+            'name', 'label', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'maximum_flow', 'heat_capacity', 'description',
         ),
         ),
     )
     )
 
 
     class Meta:
     class Meta:
         model = CoolingPortTemplate
         model = CoolingPortTemplate
         fields = [
         fields = [
-            'device_type', 'module_type', 'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow',
-            'heat_capacity', 'description',
+            'device_type', 'module_type', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit',
+            'maximum_flow', 'heat_capacity', 'description',
         ]
         ]
 
 
 
 
@@ -1279,15 +1281,17 @@ class CoolingOutletTemplateForm(ModularComponentTemplateForm):
                 FieldSet('device_type', name=_('Device Type')),
                 FieldSet('device_type', name=_('Device Type')),
                 FieldSet('module_type', name=_('Module Type')),
                 FieldSet('module_type', name=_('Module Type')),
             ),
             ),
-            'name', 'label', 'type', 'connector_type', 'diameter', 'color', 'cooling_port', 'description',
+            'name', 'label', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'color', 'cooling_port', 'description',
         ),
         ),
     )
     )
 
 
     class Meta:
     class Meta:
         model = CoolingOutletTemplate
         model = CoolingOutletTemplate
         fields = [
         fields = [
-            'device_type', 'module_type', 'name', 'label', 'type', 'connector_type', 'diameter', 'color',
-            'cooling_port', 'description',
+            'device_type', 'module_type', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit',
+            'color', 'cooling_port', 'description',
         ]
         ]
 
 
 
 
@@ -1657,16 +1661,17 @@ class PowerOutletForm(ModularDeviceComponentForm):
 class CoolingPortForm(ModularDeviceComponentForm):
 class CoolingPortForm(ModularDeviceComponentForm):
     fieldsets = (
     fieldsets = (
         FieldSet(
         FieldSet(
-            'device', 'module', 'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow',
-            'heat_capacity', 'mark_connected', 'description', 'tags',
+            'device', 'module', 'name', 'label', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'maximum_flow', 'heat_capacity', 'mark_connected', 'description', 'tags',
         ),
         ),
     )
     )
 
 
     class Meta:
     class Meta:
         model = CoolingPort
         model = CoolingPort
         fields = [
         fields = [
-            'device', 'module', 'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity',
-            'mark_connected', 'description', 'owner', 'tags',
+            'device', 'module', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow',
+            'heat_capacity', 'mark_connected', 'description', 'owner', 'tags',
         ]
         ]
 
 
 
 
@@ -1682,16 +1687,17 @@ class CoolingOutletForm(ModularDeviceComponentForm):
 
 
     fieldsets = (
     fieldsets = (
         FieldSet(
         FieldSet(
-            'device', 'module', 'name', 'label', 'type', 'connector_type', 'diameter', 'color', 'cooling_port',
-            'mark_connected', 'description', 'tags',
+            'device', 'module', 'name', 'label', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'color', 'cooling_port', 'mark_connected', 'description', 'tags',
         ),
         ),
     )
     )
 
 
     class Meta:
     class Meta:
         model = CoolingOutlet
         model = CoolingOutlet
         fields = [
         fields = [
-            'device', 'module', 'name', 'label', 'type', 'connector_type', 'diameter', 'color', 'cooling_port',
-            'mark_connected', 'description', 'tags',
+            'device', 'module', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'color',
+            'cooling_port', 'mark_connected', 'description', 'tags',
         ]
         ]
 
 
 
 

+ 3 - 2
netbox/dcim/graphql/enums.py

@@ -1,6 +1,7 @@
 import strawberry
 import strawberry
 
 
 from dcim.choices import *
 from dcim.choices import *
+from netbox.choices import DiameterUnitChoices
 
 
 __all__ = (
 __all__ = (
     'CableEndEnum',
     'CableEndEnum',
@@ -9,7 +10,6 @@ __all__ = (
     'ConsolePortSpeedEnum',
     'ConsolePortSpeedEnum',
     'ConsolePortTypeEnum',
     'ConsolePortTypeEnum',
     'CoolingConnectorTypeEnum',
     'CoolingConnectorTypeEnum',
-    'CoolingDiameterEnum',
     'CoolingFeedStatusEnum',
     'CoolingFeedStatusEnum',
     'CoolingFeedTypeEnum',
     'CoolingFeedTypeEnum',
     'CoolingMethodEnum',
     'CoolingMethodEnum',
@@ -18,6 +18,7 @@ __all__ = (
     'DeviceAirflowEnum',
     'DeviceAirflowEnum',
     'DeviceFaceEnum',
     'DeviceFaceEnum',
     'DeviceStatusEnum',
     'DeviceStatusEnum',
+    'DiameterUnitEnum',
     'FluidTypeEnum',
     'FluidTypeEnum',
     'InterfaceDuplexEnum',
     'InterfaceDuplexEnum',
     'InterfaceKindEnum',
     'InterfaceKindEnum',
@@ -56,7 +57,6 @@ CableTypeEnum = strawberry.enum(CableTypeChoices.as_enum(prefix='type'))
 ConsolePortSpeedEnum = strawberry.enum(ConsolePortSpeedChoices.as_enum(prefix='speed'))
 ConsolePortSpeedEnum = strawberry.enum(ConsolePortSpeedChoices.as_enum(prefix='speed'))
 ConsolePortTypeEnum = strawberry.enum(ConsolePortTypeChoices.as_enum(prefix='type'))
 ConsolePortTypeEnum = strawberry.enum(ConsolePortTypeChoices.as_enum(prefix='type'))
 CoolingConnectorTypeEnum = strawberry.enum(CoolingConnectorTypeChoices.as_enum(prefix='type'))
 CoolingConnectorTypeEnum = strawberry.enum(CoolingConnectorTypeChoices.as_enum(prefix='type'))
-CoolingDiameterEnum = strawberry.enum(CoolingDiameterChoices.as_enum(prefix='diameter'))
 CoolingFeedStatusEnum = strawberry.enum(CoolingFeedStatusChoices.as_enum(prefix='status'))
 CoolingFeedStatusEnum = strawberry.enum(CoolingFeedStatusChoices.as_enum(prefix='status'))
 CoolingFeedTypeEnum = strawberry.enum(CoolingFeedTypeChoices.as_enum(prefix='type'))
 CoolingFeedTypeEnum = strawberry.enum(CoolingFeedTypeChoices.as_enum(prefix='type'))
 CoolingMethodEnum = strawberry.enum(CoolingMethodChoices.as_enum(prefix='method'))
 CoolingMethodEnum = strawberry.enum(CoolingMethodChoices.as_enum(prefix='method'))
@@ -65,6 +65,7 @@ CoolingSourceTypeEnum = strawberry.enum(CoolingSourceTypeChoices.as_enum(prefix=
 DeviceAirflowEnum = strawberry.enum(DeviceAirflowChoices.as_enum(prefix='airflow'))
 DeviceAirflowEnum = strawberry.enum(DeviceAirflowChoices.as_enum(prefix='airflow'))
 DeviceFaceEnum = strawberry.enum(DeviceFaceChoices.as_enum(prefix='face'))
 DeviceFaceEnum = strawberry.enum(DeviceFaceChoices.as_enum(prefix='face'))
 DeviceStatusEnum = strawberry.enum(DeviceStatusChoices.as_enum(prefix='status'))
 DeviceStatusEnum = strawberry.enum(DeviceStatusChoices.as_enum(prefix='status'))
+DiameterUnitEnum = strawberry.enum(DiameterUnitChoices.as_enum(prefix='unit'))
 FluidTypeEnum = strawberry.enum(FluidTypeChoices.as_enum(prefix='fluid'))
 FluidTypeEnum = strawberry.enum(FluidTypeChoices.as_enum(prefix='fluid'))
 InterfaceDuplexEnum = strawberry.enum(InterfaceDuplexChoices.as_enum(prefix='duplex'))
 InterfaceDuplexEnum = strawberry.enum(InterfaceDuplexChoices.as_enum(prefix='duplex'))
 InterfaceKindEnum = strawberry.enum(InterfaceKindChoices.as_enum(prefix='kind'))
 InterfaceKindEnum = strawberry.enum(InterfaceKindChoices.as_enum(prefix='kind'))

+ 16 - 4
netbox/dcim/graphql/filters.py

@@ -980,7 +980,10 @@ class CoolingOutletFilter(ModularComponentFilterMixin, CabledObjectModelFilterMi
     connector_type: BaseFilterLookup[
     connector_type: BaseFilterLookup[
         Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
         Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
     ] | None = strawberry_django.filter_field()
     ] | None = strawberry_django.filter_field()
-    diameter: BaseFilterLookup[Annotated['CoolingDiameterEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
+    diameter: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
+        strawberry_django.filter_field()
+    )
+    diameter_unit: BaseFilterLookup[Annotated['DiameterUnitEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
         strawberry_django.filter_field()
         strawberry_django.filter_field()
     )
     )
     cooling_port: Annotated['CoolingPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
     cooling_port: Annotated['CoolingPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
@@ -1000,7 +1003,10 @@ class CoolingOutletTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLog
     connector_type: BaseFilterLookup[
     connector_type: BaseFilterLookup[
         Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
         Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
     ] | None = strawberry_django.filter_field()
     ] | None = strawberry_django.filter_field()
-    diameter: BaseFilterLookup[Annotated['CoolingDiameterEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
+    diameter: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
+        strawberry_django.filter_field()
+    )
+    diameter_unit: BaseFilterLookup[Annotated['DiameterUnitEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
         strawberry_django.filter_field()
         strawberry_django.filter_field()
     )
     )
     cooling_port: Annotated['CoolingPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
     cooling_port: Annotated['CoolingPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
@@ -1017,7 +1023,10 @@ class CoolingPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixi
     connector_type: BaseFilterLookup[
     connector_type: BaseFilterLookup[
         Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
         Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
     ] | None = strawberry_django.filter_field()
     ] | None = strawberry_django.filter_field()
-    diameter: BaseFilterLookup[Annotated['CoolingDiameterEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
+    diameter: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
+        strawberry_django.filter_field()
+    )
+    diameter_unit: BaseFilterLookup[Annotated['DiameterUnitEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
         strawberry_django.filter_field()
         strawberry_django.filter_field()
     )
     )
     maximum_flow: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
     maximum_flow: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
@@ -1036,7 +1045,10 @@ class CoolingPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLogge
     connector_type: BaseFilterLookup[
     connector_type: BaseFilterLookup[
         Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
         Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
     ] | None = strawberry_django.filter_field()
     ] | None = strawberry_django.filter_field()
-    diameter: BaseFilterLookup[Annotated['CoolingDiameterEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
+    diameter: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
+        strawberry_django.filter_field()
+    )
+    diameter_unit: BaseFilterLookup[Annotated['DiameterUnitEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
         strawberry_django.filter_field()
         strawberry_django.filter_field()
     )
     )
     maximum_flow: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
     maximum_flow: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (

+ 65 - 5
netbox/dcim/migrations/0241_device_cooling_method_device_cooling_outlet_count_and_more.py

@@ -1,4 +1,4 @@
-# Generated by Django 6.0.6 on 2026-06-23 18:45
+# Generated by Django 6.0.6 on 2026-06-23 20:57
 
 
 import django.contrib.postgres.fields
 import django.contrib.postgres.fields
 import django.core.validators
 import django.core.validators
@@ -140,6 +140,22 @@ class Migration(migrations.Migration):
                         encoder=utilities.json.CustomFieldJSONEncoder,
                         encoder=utilities.json.CustomFieldJSONEncoder,
                     ),
                     ),
                 ),
                 ),
+                (
+                    "diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=2, max_digits=8, null=True
+                    ),
+                ),
+                (
+                    "diameter_unit",
+                    models.CharField(blank=True, max_length=50, null=True),
+                ),
+                (
+                    "_abs_diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=4, max_digits=13, null=True
+                    ),
+                ),
                 ("name", models.CharField(db_collation="natural_sort", max_length=64)),
                 ("name", models.CharField(db_collation="natural_sort", max_length=64)),
                 ("label", models.CharField(blank=True, max_length=64)),
                 ("label", models.CharField(blank=True, max_length=64)),
                 ("description", models.CharField(blank=True, max_length=200)),
                 ("description", models.CharField(blank=True, max_length=200)),
@@ -174,7 +190,6 @@ class Migration(migrations.Migration):
                     "connector_type",
                     "connector_type",
                     models.CharField(blank=True, max_length=50, null=True),
                     models.CharField(blank=True, max_length=50, null=True),
                 ),
                 ),
-                ("diameter", models.CharField(blank=True, max_length=50, null=True)),
                 (
                 (
                     "maximum_flow",
                     "maximum_flow",
                     models.DecimalField(
                     models.DecimalField(
@@ -310,6 +325,22 @@ class Migration(migrations.Migration):
                         encoder=utilities.json.CustomFieldJSONEncoder,
                         encoder=utilities.json.CustomFieldJSONEncoder,
                     ),
                     ),
                 ),
                 ),
+                (
+                    "diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=2, max_digits=8, null=True
+                    ),
+                ),
+                (
+                    "diameter_unit",
+                    models.CharField(blank=True, max_length=50, null=True),
+                ),
+                (
+                    "_abs_diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=4, max_digits=13, null=True
+                    ),
+                ),
                 ("name", models.CharField(db_collation="natural_sort", max_length=64)),
                 ("name", models.CharField(db_collation="natural_sort", max_length=64)),
                 ("label", models.CharField(blank=True, max_length=64)),
                 ("label", models.CharField(blank=True, max_length=64)),
                 ("description", models.CharField(blank=True, max_length=200)),
                 ("description", models.CharField(blank=True, max_length=200)),
@@ -344,7 +375,6 @@ class Migration(migrations.Migration):
                     "connector_type",
                     "connector_type",
                     models.CharField(blank=True, max_length=50, null=True),
                     models.CharField(blank=True, max_length=50, null=True),
                 ),
                 ),
-                ("diameter", models.CharField(blank=True, max_length=50, null=True)),
                 ("color", utilities.fields.ColorField(blank=True, max_length=6)),
                 ("color", utilities.fields.ColorField(blank=True, max_length=6)),
                 (
                 (
                     "_location",
                     "_location",
@@ -463,6 +493,22 @@ class Migration(migrations.Migration):
                 ),
                 ),
                 ("created", models.DateTimeField(auto_now_add=True, null=True)),
                 ("created", models.DateTimeField(auto_now_add=True, null=True)),
                 ("last_updated", models.DateTimeField(auto_now=True, null=True)),
                 ("last_updated", models.DateTimeField(auto_now=True, null=True)),
+                (
+                    "diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=2, max_digits=8, null=True
+                    ),
+                ),
+                (
+                    "diameter_unit",
+                    models.CharField(blank=True, max_length=50, null=True),
+                ),
+                (
+                    "_abs_diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=4, max_digits=13, null=True
+                    ),
+                ),
                 ("name", models.CharField(db_collation="natural_sort", max_length=64)),
                 ("name", models.CharField(db_collation="natural_sort", max_length=64)),
                 ("label", models.CharField(blank=True, max_length=64)),
                 ("label", models.CharField(blank=True, max_length=64)),
                 ("description", models.CharField(blank=True, max_length=200)),
                 ("description", models.CharField(blank=True, max_length=200)),
@@ -471,7 +517,6 @@ class Migration(migrations.Migration):
                     "connector_type",
                     "connector_type",
                     models.CharField(blank=True, max_length=50, null=True),
                     models.CharField(blank=True, max_length=50, null=True),
                 ),
                 ),
-                ("diameter", models.CharField(blank=True, max_length=50, null=True)),
                 (
                 (
                     "maximum_flow",
                     "maximum_flow",
                     models.DecimalField(
                     models.DecimalField(
@@ -536,6 +581,22 @@ class Migration(migrations.Migration):
                 ),
                 ),
                 ("created", models.DateTimeField(auto_now_add=True, null=True)),
                 ("created", models.DateTimeField(auto_now_add=True, null=True)),
                 ("last_updated", models.DateTimeField(auto_now=True, null=True)),
                 ("last_updated", models.DateTimeField(auto_now=True, null=True)),
+                (
+                    "diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=2, max_digits=8, null=True
+                    ),
+                ),
+                (
+                    "diameter_unit",
+                    models.CharField(blank=True, max_length=50, null=True),
+                ),
+                (
+                    "_abs_diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=4, max_digits=13, null=True
+                    ),
+                ),
                 ("name", models.CharField(db_collation="natural_sort", max_length=64)),
                 ("name", models.CharField(db_collation="natural_sort", max_length=64)),
                 ("label", models.CharField(blank=True, max_length=64)),
                 ("label", models.CharField(blank=True, max_length=64)),
                 ("description", models.CharField(blank=True, max_length=200)),
                 ("description", models.CharField(blank=True, max_length=200)),
@@ -544,7 +605,6 @@ class Migration(migrations.Migration):
                     "connector_type",
                     "connector_type",
                     models.CharField(blank=True, max_length=50, null=True),
                     models.CharField(blank=True, max_length=50, null=True),
                 ),
                 ),
-                ("diameter", models.CharField(blank=True, max_length=50, null=True)),
                 ("color", utilities.fields.ColorField(blank=True, max_length=6)),
                 ("color", utilities.fields.ColorField(blank=True, max_length=6)),
                 (
                 (
                     "device_type",
                     "device_type",

+ 11 - 18
netbox/dcim/models/device_component_templates.py

@@ -12,6 +12,7 @@ from dcim.models.mixins import InterfaceValidationMixin
 from dcim.utils import get_module_bay_positions, resolve_module_placeholder
 from dcim.utils import get_module_bay_positions, resolve_module_placeholder
 from netbox.models import ChangeLoggedModel
 from netbox.models import ChangeLoggedModel
 from netbox.models.ltree import LtreeManager, LtreeModel
 from netbox.models.ltree import LtreeManager, LtreeModel
+from netbox.models.mixins import DiameterMixin
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.ordering import naturalize_interface
 from utilities.ordering import naturalize_interface
 from utilities.tracking import TrackingModelMixin
 from utilities.tracking import TrackingModelMixin
@@ -436,7 +437,7 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
         }
         }
 
 
 
 
-class CoolingPortTemplate(ModularComponentTemplateModel):
+class CoolingPortTemplate(DiameterMixin, ModularComponentTemplateModel):
     """
     """
     A template for a CoolingPort to be created for a new Device.
     A template for a CoolingPort to be created for a new Device.
     """
     """
@@ -454,13 +455,7 @@ class CoolingPortTemplate(ModularComponentTemplateModel):
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
-    diameter = models.CharField(
-        verbose_name=_('diameter'),
-        max_length=50,
-        choices=CoolingDiameterChoices,
-        blank=True,
-        null=True
-    )
+    # diameter, diameter_unit, _abs_diameter provided by DiameterMixin
     maximum_flow = models.DecimalField(
     maximum_flow = models.DecimalField(
         verbose_name=_('maximum flow'),
         verbose_name=_('maximum flow'),
         max_digits=8,
         max_digits=8,
@@ -493,6 +488,7 @@ class CoolingPortTemplate(ModularComponentTemplateModel):
             type=self.type,
             type=self.type,
             connector_type=self.connector_type,
             connector_type=self.connector_type,
             diameter=self.diameter,
             diameter=self.diameter,
+            diameter_unit=self.diameter_unit,
             maximum_flow=self.maximum_flow,
             maximum_flow=self.maximum_flow,
             heat_capacity=self.heat_capacity,
             heat_capacity=self.heat_capacity,
             **kwargs
             **kwargs
@@ -504,7 +500,8 @@ class CoolingPortTemplate(ModularComponentTemplateModel):
             'name': self.name,
             'name': self.name,
             'type': self.type,
             'type': self.type,
             'connector_type': self.connector_type,
             'connector_type': self.connector_type,
-            'diameter': self.diameter,
+            'diameter': float(self.diameter) if self.diameter is not None else None,
+            'diameter_unit': self.diameter_unit,
             'maximum_flow': float(self.maximum_flow) if self.maximum_flow is not None else None,
             'maximum_flow': float(self.maximum_flow) if self.maximum_flow is not None else None,
             'heat_capacity': float(self.heat_capacity) if self.heat_capacity is not None else None,
             'heat_capacity': float(self.heat_capacity) if self.heat_capacity is not None else None,
             'label': self.label,
             'label': self.label,
@@ -512,7 +509,7 @@ class CoolingPortTemplate(ModularComponentTemplateModel):
         }
         }
 
 
 
 
-class CoolingOutletTemplate(ModularComponentTemplateModel):
+class CoolingOutletTemplate(DiameterMixin, ModularComponentTemplateModel):
     """
     """
     A template for a CoolingOutlet to be created for a new Device.
     A template for a CoolingOutlet to be created for a new Device.
     """
     """
@@ -530,13 +527,7 @@ class CoolingOutletTemplate(ModularComponentTemplateModel):
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
-    diameter = models.CharField(
-        verbose_name=_('diameter'),
-        max_length=50,
-        choices=CoolingDiameterChoices,
-        blank=True,
-        null=True
-    )
+    # diameter, diameter_unit, _abs_diameter provided by DiameterMixin
     color = ColorField(
     color = ColorField(
         verbose_name=_('color'),
         verbose_name=_('color'),
         blank=True
         blank=True
@@ -585,6 +576,7 @@ class CoolingOutletTemplate(ModularComponentTemplateModel):
             type=self.type,
             type=self.type,
             connector_type=self.connector_type,
             connector_type=self.connector_type,
             diameter=self.diameter,
             diameter=self.diameter,
+            diameter_unit=self.diameter_unit,
             color=self.color,
             color=self.color,
             cooling_port=cooling_port,
             cooling_port=cooling_port,
             **kwargs
             **kwargs
@@ -596,7 +588,8 @@ class CoolingOutletTemplate(ModularComponentTemplateModel):
             'name': self.name,
             'name': self.name,
             'type': self.type,
             'type': self.type,
             'connector_type': self.connector_type,
             'connector_type': self.connector_type,
-            'diameter': self.diameter,
+            'diameter': float(self.diameter) if self.diameter is not None else None,
+            'diameter_unit': self.diameter_unit,
             'color': self.color,
             'color': self.color,
             'cooling_port': self.cooling_port.name if self.cooling_port else None,
             'cooling_port': self.cooling_port.name if self.cooling_port else None,
             'label': self.label,
             'label': self.label,

+ 9 - 21
netbox/dcim/models/device_components.py

@@ -16,7 +16,7 @@ from dcim.models.mixins import InterfaceValidationMixin
 from netbox.choices import ColorChoices
 from netbox.choices import ColorChoices
 from netbox.models import NetBoxModel, OrganizationalModel
 from netbox.models import NetBoxModel, OrganizationalModel
 from netbox.models.ltree import LtreeManager, LtreeModel, SortPathField
 from netbox.models.ltree import LtreeManager, LtreeModel, SortPathField
-from netbox.models.mixins import OwnerMixin
+from netbox.models.mixins import DiameterMixin, OwnerMixin
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.ordering import naturalize_interface
 from utilities.ordering import naturalize_interface
 from utilities.query_functions import CollateAsChar
 from utilities.query_functions import CollateAsChar
@@ -689,7 +689,7 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
 # Cooling components
 # Cooling components
 #
 #
 
 
-class CoolingPort(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
+class CoolingPort(DiameterMixin, ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
     """
     """
     A coolant intake/outlet port within a Device (e.g. a server cold-plate inlet or CDU intake).
     A coolant intake/outlet port within a Device (e.g. a server cold-plate inlet or CDU intake).
     CoolingPorts connect to CoolingOutlets.
     CoolingPorts connect to CoolingOutlets.
@@ -710,14 +710,7 @@ class CoolingPort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
         null=True,
         null=True,
         help_text=_('Physical connector type')
         help_text=_('Physical connector type')
     )
     )
-    diameter = models.CharField(
-        verbose_name=_('diameter'),
-        max_length=50,
-        choices=CoolingDiameterChoices,
-        blank=True,
-        null=True,
-        help_text=_('Nominal connector diameter')
-    )
+    # diameter, diameter_unit, _abs_diameter provided by DiameterMixin
     maximum_flow = models.DecimalField(
     maximum_flow = models.DecimalField(
         verbose_name=_('maximum flow'),
         verbose_name=_('maximum flow'),
         max_digits=8,
         max_digits=8,
@@ -737,7 +730,9 @@ class CoolingPort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
         help_text=_('Heat removal capacity (kW)')
         help_text=_('Heat removal capacity (kW)')
     )
     )
 
 
-    clone_fields = ('device', 'module', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity')
+    clone_fields = (
+        'device', 'module', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity',
+    )
 
 
     class Meta(ModularComponentModel.Meta):
     class Meta(ModularComponentModel.Meta):
         verbose_name = _('cooling port')
         verbose_name = _('cooling port')
@@ -747,7 +742,7 @@ class CoolingPort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
         return CoolingFeedTypeChoices.colors.get(self.type)
         return CoolingFeedTypeChoices.colors.get(self.type)
 
 
 
 
-class CoolingOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
+class CoolingOutlet(DiameterMixin, ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
     """
     """
     A coolant outlet within a Device (e.g. a CDU or manifold outlet) which feeds a CoolingPort.
     A coolant outlet within a Device (e.g. a CDU or manifold outlet) which feeds a CoolingPort.
     """
     """
@@ -767,14 +762,7 @@ class CoolingOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Trac
         null=True,
         null=True,
         help_text=_('Physical connector type')
         help_text=_('Physical connector type')
     )
     )
-    diameter = models.CharField(
-        verbose_name=_('diameter'),
-        max_length=50,
-        choices=CoolingDiameterChoices,
-        blank=True,
-        null=True,
-        help_text=_('Nominal connector diameter')
-    )
+    # diameter, diameter_unit, _abs_diameter provided by DiameterMixin
     cooling_port = models.ForeignKey(
     cooling_port = models.ForeignKey(
         to='dcim.CoolingPort',
         to='dcim.CoolingPort',
         on_delete=models.SET_NULL,
         on_delete=models.SET_NULL,
@@ -787,7 +775,7 @@ class CoolingOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Trac
         blank=True
         blank=True
     )
     )
 
 
-    clone_fields = ('device', 'module', 'type', 'connector_type', 'diameter', 'cooling_port')
+    clone_fields = ('device', 'module', 'type', 'connector_type', 'diameter', 'diameter_unit', 'cooling_port')
 
 
     class Meta(ModularComponentModel.Meta):
     class Meta(ModularComponentModel.Meta):
         verbose_name = _('cooling outlet')
         verbose_name = _('cooling outlet')

+ 22 - 3
netbox/dcim/tables/cooling.py

@@ -9,7 +9,12 @@ from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
 
 
 from .devices import CableTerminationTable, DeviceComponentTable, ModularDeviceComponentTable, PathEndpointTable
 from .devices import CableTerminationTable, DeviceComponentTable, ModularDeviceComponentTable, PathEndpointTable
 from .devicetypes import ComponentTemplateTable
 from .devicetypes import ComponentTemplateTable
-from .template_code import COOLINGOUTLET_BUTTONS, COOLINGPORT_BUTTONS, MODULAR_COMPONENT_TEMPLATE_BUTTONS
+from .template_code import (
+    COOLINGOUTLET_BUTTONS,
+    COOLINGPORT_BUTTONS,
+    DIAMETER,
+    MODULAR_COMPONENT_TEMPLATE_BUTTONS,
+)
 
 
 __all__ = (
 __all__ = (
     'CoolingFeedTable',
     'CoolingFeedTable',
@@ -164,8 +169,10 @@ class CoolingPortTable(ModularDeviceComponentTable, PathEndpointTable):
     connector_type = columns.ChoiceFieldColumn(
     connector_type = columns.ChoiceFieldColumn(
         verbose_name=_('Connector Type'),
         verbose_name=_('Connector Type'),
     )
     )
-    diameter = columns.ChoiceFieldColumn(
+    diameter = columns.TemplateColumn(
         verbose_name=_('Diameter'),
         verbose_name=_('Diameter'),
+        template_code=DIAMETER,
+        order_by=('_abs_diameter', 'diameter_unit')
     )
     )
     maximum_flow = tables.Column(
     maximum_flow = tables.Column(
         verbose_name=_('Maximum flow (L/min)')
         verbose_name=_('Maximum flow (L/min)')
@@ -208,8 +215,10 @@ class CoolingOutletTable(ModularDeviceComponentTable, PathEndpointTable):
     connector_type = columns.ChoiceFieldColumn(
     connector_type = columns.ChoiceFieldColumn(
         verbose_name=_('Connector Type'),
         verbose_name=_('Connector Type'),
     )
     )
-    diameter = columns.ChoiceFieldColumn(
+    diameter = columns.TemplateColumn(
         verbose_name=_('Diameter'),
         verbose_name=_('Diameter'),
+        template_code=DIAMETER,
+        order_by=('_abs_diameter', 'diameter_unit')
     )
     )
     cooling_port = tables.Column(
     cooling_port = tables.Column(
         verbose_name=_('Cooling Port'),
         verbose_name=_('Cooling Port'),
@@ -238,6 +247,11 @@ class CoolingOutletTable(ModularDeviceComponentTable, PathEndpointTable):
 #
 #
 
 
 class CoolingPortTemplateTable(ComponentTemplateTable):
 class CoolingPortTemplateTable(ComponentTemplateTable):
+    diameter = columns.TemplateColumn(
+        verbose_name=_('Diameter'),
+        template_code=DIAMETER,
+        order_by=('_abs_diameter', 'diameter_unit')
+    )
     actions = columns.ActionsColumn(
     actions = columns.ActionsColumn(
         actions=('edit', 'delete'),
         actions=('edit', 'delete'),
         extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
         extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
@@ -257,6 +271,11 @@ class CoolingPortTemplateTable(ComponentTemplateTable):
 #
 #
 
 
 class CoolingOutletTemplateTable(ComponentTemplateTable):
 class CoolingOutletTemplateTable(ComponentTemplateTable):
+    diameter = columns.TemplateColumn(
+        verbose_name=_('Diameter'),
+        template_code=DIAMETER,
+        order_by=('_abs_diameter', 'diameter_unit')
+    )
     color = columns.ColorColumn(
     color = columns.ColorColumn(
         verbose_name=_('Color'),
         verbose_name=_('Color'),
     )
     )

+ 4 - 0
netbox/dcim/tables/template_code.py

@@ -52,6 +52,10 @@ WEIGHT = """
 {% display_weight record.weight record.weight_unit record.abs_weight %}
 {% display_weight record.weight record.weight_unit record.abs_weight %}
 """
 """
 
 
+DIAMETER = """
+{% if record.diameter %}{{ record.diameter }} {{ record.get_diameter_unit_display }}{% endif %}
+"""
+
 DEVICE_LINK = """
 DEVICE_LINK = """
 {{ record.label|default:'<span class="badge text-bg-info">Unnamed device</span>' }}
 {{ record.label|default:'<span class="badge text-bg-info">Unnamed device</span>' }}
 """
 """

+ 31 - 17
netbox/dcim/tests/test_filtersets.py

@@ -1,3 +1,5 @@
+from decimal import Decimal
+
 from django.conf import settings
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 from django.test import TestCase
@@ -8,7 +10,7 @@ from dcim.filtersets import *
 from dcim.models import *
 from dcim.models import *
 from ipam.choices import VLANQinQRoleChoices
 from ipam.choices import VLANQinQRoleChoices
 from ipam.models import ASN, RIR, VLAN, VRF, IPAddress, VLANTranslationPolicy
 from ipam.models import ASN, RIR, VLAN, VRF, IPAddress, VLANTranslationPolicy
-from netbox.choices import ColorChoices, WeightUnitChoices
+from netbox.choices import ColorChoices, DiameterUnitChoices, WeightUnitChoices
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from users.models import User
 from users.models import User
 from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
 from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
@@ -2048,7 +2050,8 @@ class CoolingPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTest
                 name='Cooling Port 1',
                 name='Cooling Port 1',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-                diameter=CoolingDiameterChoices.DN25,
+                diameter=Decimal('25'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=100,
                 maximum_flow=100,
                 heat_capacity=50,
                 heat_capacity=50,
                 description='foobar1'
                 description='foobar1'
@@ -2058,7 +2061,8 @@ class CoolingPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTest
                 name='Cooling Port 2',
                 name='Cooling Port 2',
                 type=CoolingFeedTypeChoices.TYPE_RETURN,
                 type=CoolingFeedTypeChoices.TYPE_RETURN,
                 connector_type=CoolingConnectorTypeChoices.TYPE_QDC,
                 connector_type=CoolingConnectorTypeChoices.TYPE_QDC,
-                diameter=CoolingDiameterChoices.DN32,
+                diameter=Decimal('32'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=200,
                 maximum_flow=200,
                 heat_capacity=100,
                 heat_capacity=100,
                 description='foobar2'
                 description='foobar2'
@@ -2068,7 +2072,8 @@ class CoolingPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTest
                 name='Cooling Port 3',
                 name='Cooling Port 3',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQDB,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQDB,
-                diameter=CoolingDiameterChoices.DN40,
+                diameter=Decimal('40'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=300,
                 maximum_flow=300,
                 heat_capacity=150,
                 heat_capacity=150,
                 description='foobar3'
                 description='foobar3'
@@ -2088,7 +2093,7 @@ class CoolingPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTest
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_diameter(self):
     def test_diameter(self):
-        params = {'diameter': CoolingDiameterChoices.DN25}
+        params = {'diameter': [Decimal('25')]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_maximum_flow(self):
     def test_maximum_flow(self):
@@ -2122,7 +2127,8 @@ class CoolingOutletTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTe
                 name='Cooling Outlet 1',
                 name='Cooling Outlet 1',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-                diameter=CoolingDiameterChoices.DN25,
+                diameter=Decimal('25'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 color=ColorChoices.COLOR_RED,
                 color=ColorChoices.COLOR_RED,
                 description='foobar1'
                 description='foobar1'
             ),
             ),
@@ -2131,7 +2137,8 @@ class CoolingOutletTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTe
                 name='Cooling Outlet 2',
                 name='Cooling Outlet 2',
                 type=CoolingFeedTypeChoices.TYPE_RETURN,
                 type=CoolingFeedTypeChoices.TYPE_RETURN,
                 connector_type=CoolingConnectorTypeChoices.TYPE_QDC,
                 connector_type=CoolingConnectorTypeChoices.TYPE_QDC,
-                diameter=CoolingDiameterChoices.DN32,
+                diameter=Decimal('32'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 color=ColorChoices.COLOR_GREEN,
                 color=ColorChoices.COLOR_GREEN,
                 description='foobar2'
                 description='foobar2'
             ),
             ),
@@ -2140,7 +2147,8 @@ class CoolingOutletTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTe
                 name='Cooling Outlet 3',
                 name='Cooling Outlet 3',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQDB,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQDB,
-                diameter=CoolingDiameterChoices.DN40,
+                diameter=Decimal('40'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 color=ColorChoices.COLOR_BLUE,
                 color=ColorChoices.COLOR_BLUE,
                 description='foobar3'
                 description='foobar3'
             ),
             ),
@@ -2159,7 +2167,7 @@ class CoolingOutletTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTe
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_diameter(self):
     def test_diameter(self):
-        params = {'diameter': CoolingDiameterChoices.DN25}
+        params = {'diameter': [Decimal('25')]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_color(self):
     def test_color(self):
@@ -4792,7 +4800,8 @@ class CoolingPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
                 label='A',
                 label='A',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-                diameter=CoolingDiameterChoices.DN25,
+                diameter=Decimal('25'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=100,
                 maximum_flow=100,
                 heat_capacity=50,
                 heat_capacity=50,
                 description='First',
                 description='First',
@@ -4807,7 +4816,8 @@ class CoolingPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
                 label='B',
                 label='B',
                 type=CoolingFeedTypeChoices.TYPE_RETURN,
                 type=CoolingFeedTypeChoices.TYPE_RETURN,
                 connector_type=CoolingConnectorTypeChoices.TYPE_QDC,
                 connector_type=CoolingConnectorTypeChoices.TYPE_QDC,
-                diameter=CoolingDiameterChoices.DN32,
+                diameter=Decimal('32'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=200,
                 maximum_flow=200,
                 heat_capacity=100,
                 heat_capacity=100,
                 description='Second',
                 description='Second',
@@ -4822,7 +4832,8 @@ class CoolingPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
                 label='C',
                 label='C',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQDB,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQDB,
-                diameter=CoolingDiameterChoices.DN40,
+                diameter=Decimal('40'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=300,
                 maximum_flow=300,
                 heat_capacity=150,
                 heat_capacity=150,
                 description='Third',
                 description='Third',
@@ -4859,7 +4870,7 @@ class CoolingPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_diameter(self):
     def test_diameter(self):
-        params = {'diameter': [CoolingDiameterChoices.DN25, CoolingDiameterChoices.DN32]}
+        params = {'diameter': [Decimal('25'), Decimal('32')]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_maximum_flow(self):
     def test_maximum_flow(self):
@@ -5078,7 +5089,8 @@ class CoolingOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLogge
                 label='A',
                 label='A',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-                diameter=CoolingDiameterChoices.DN25,
+                diameter=Decimal('25'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 description='First',
                 description='First',
                 color='ff0000',
                 color='ff0000',
                 _site=devices[0].site,
                 _site=devices[0].site,
@@ -5092,7 +5104,8 @@ class CoolingOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLogge
                 label='B',
                 label='B',
                 type=CoolingFeedTypeChoices.TYPE_RETURN,
                 type=CoolingFeedTypeChoices.TYPE_RETURN,
                 connector_type=CoolingConnectorTypeChoices.TYPE_QDC,
                 connector_type=CoolingConnectorTypeChoices.TYPE_QDC,
-                diameter=CoolingDiameterChoices.DN32,
+                diameter=Decimal('32'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 description='Second',
                 description='Second',
                 color='00ff00',
                 color='00ff00',
                 _site=devices[1].site,
                 _site=devices[1].site,
@@ -5106,7 +5119,8 @@ class CoolingOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLogge
                 label='C',
                 label='C',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQDB,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQDB,
-                diameter=CoolingDiameterChoices.DN40,
+                diameter=Decimal('40'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 description='Third',
                 description='Third',
                 color='0000ff',
                 color='0000ff',
                 _site=devices[2].site,
                 _site=devices[2].site,
@@ -5146,7 +5160,7 @@ class CoolingOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLogge
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_diameter(self):
     def test_diameter(self):
-        params = {'diameter': [CoolingDiameterChoices.DN25, CoolingDiameterChoices.DN32]}
+        params = {'diameter': [Decimal('25'), Decimal('32')]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_region(self):
     def test_region(self):

+ 11 - 5
netbox/dcim/tests/test_models.py

@@ -1,3 +1,5 @@
+from decimal import Decimal
+
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.db.models.signals import post_save
 from django.db.models.signals import post_save
 from django.test import TestCase, tag
 from django.test import TestCase, tag
@@ -9,7 +11,7 @@ from dcim.models import *
 from extras.events import serialize_for_event
 from extras.events import serialize_for_event
 from extras.models import CustomField
 from extras.models import CustomField
 from ipam.models import Prefix
 from ipam.models import Prefix
-from netbox.choices import WeightUnitChoices
+from netbox.choices import DiameterUnitChoices, WeightUnitChoices
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.data import drange
 from utilities.data import drange
 from virtualization.models import Cluster, ClusterType
 from virtualization.models import Cluster, ClusterType
@@ -2859,7 +2861,8 @@ class CoolingComponentTestCase(TestCase):
             name='Cooling Port 1',
             name='Cooling Port 1',
             type=CoolingFeedTypeChoices.TYPE_SUPPLY,
             type=CoolingFeedTypeChoices.TYPE_SUPPLY,
             connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
             connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-            diameter=CoolingDiameterChoices.DN25,
+            diameter=Decimal('25'),
+            diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
             maximum_flow=100,
             maximum_flow=100,
             heat_capacity=50
             heat_capacity=50
         )
         )
@@ -2868,7 +2871,8 @@ class CoolingComponentTestCase(TestCase):
             name='Cooling Outlet 1',
             name='Cooling Outlet 1',
             type=CoolingFeedTypeChoices.TYPE_SUPPLY,
             type=CoolingFeedTypeChoices.TYPE_SUPPLY,
             connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
             connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-            diameter=CoolingDiameterChoices.DN25
+            diameter=Decimal('25'),
+            diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER
         )
         )
 
 
         device = Device.objects.create(
         device = Device.objects.create(
@@ -2883,7 +2887,8 @@ class CoolingComponentTestCase(TestCase):
             name='Cooling Port 1',
             name='Cooling Port 1',
             type=CoolingFeedTypeChoices.TYPE_SUPPLY,
             type=CoolingFeedTypeChoices.TYPE_SUPPLY,
             connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
             connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-            diameter=CoolingDiameterChoices.DN25,
+            diameter=Decimal('25'),
+            diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
             maximum_flow=100,
             maximum_flow=100,
             heat_capacity=50
             heat_capacity=50
         )
         )
@@ -2894,7 +2899,8 @@ class CoolingComponentTestCase(TestCase):
             name='Cooling Outlet 1',
             name='Cooling Outlet 1',
             type=CoolingFeedTypeChoices.TYPE_SUPPLY,
             type=CoolingFeedTypeChoices.TYPE_SUPPLY,
             connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
             connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-            diameter=CoolingDiameterChoices.DN25
+            diameter=Decimal('25'),
+            diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER
         )
         )
 
 
     def test_cooling_outlet_clean_different_device(self):
     def test_cooling_outlet_clean_different_device(self):

+ 23 - 12
netbox/dcim/tests/test_views.py

@@ -18,7 +18,7 @@ from dcim.constants import *
 from dcim.models import *
 from dcim.models import *
 from extras.models import ConfigTemplate
 from extras.models import ConfigTemplate
 from ipam.models import ASN, RIR, VLAN, VRF
 from ipam.models import ASN, RIR, VLAN, VRF
-from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, WeightUnitChoices
+from netbox.choices import CSVDelimiterChoices, DiameterUnitChoices, ImportFormatChoices, WeightUnitChoices
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from users.models import ObjectPermission, User
 from users.models import ObjectPermission, User
 from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
 from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
@@ -4236,7 +4236,8 @@ class CoolingPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
             'name': 'Cooling Port Template X',
             'name': 'Cooling Port Template X',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
             'maximum_flow': 100,
             'heat_capacity': 50,
             'heat_capacity': 50,
         }
         }
@@ -4246,7 +4247,8 @@ class CoolingPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
             'name': 'Cooling Port Template [4-6]',
             'name': 'Cooling Port Template [4-6]',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
             'maximum_flow': 100,
             'heat_capacity': 50,
             'heat_capacity': 50,
         }
         }
@@ -4254,7 +4256,8 @@ class CoolingPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
             'maximum_flow': 100,
             'heat_capacity': 50,
             'heat_capacity': 50,
         }
         }
@@ -4285,7 +4288,8 @@ class CoolingOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTes
             'name': 'Cooling Outlet Template X',
             'name': 'Cooling Outlet Template X',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'cooling_port': coolingports[0].pk,
             'cooling_port': coolingports[0].pk,
         }
         }
 
 
@@ -4294,14 +4298,16 @@ class CoolingOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTes
             'name': 'Cooling Outlet Template [4-6]',
             'name': 'Cooling Outlet Template [4-6]',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'cooling_port': coolingports[0].pk,
             'cooling_port': coolingports[0].pk,
         }
         }
 
 
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
         }
         }
 
 
 
 
@@ -4327,7 +4333,8 @@ class CoolingPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'name': 'Cooling Port X',
             'name': 'Cooling Port X',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
             'maximum_flow': 100,
             'heat_capacity': 50,
             'heat_capacity': 50,
             'description': 'A cooling port',
             'description': 'A cooling port',
@@ -4339,7 +4346,8 @@ class CoolingPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'name': 'Cooling Port [4-6]]',
             'name': 'Cooling Port [4-6]]',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
             'maximum_flow': 100,
             'heat_capacity': 50,
             'heat_capacity': 50,
             'description': 'A cooling port',
             'description': 'A cooling port',
@@ -4349,7 +4357,8 @@ class CoolingPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
             'maximum_flow': 100,
             'heat_capacity': 50,
             'heat_capacity': 50,
             'description': 'New description',
             'description': 'New description',
@@ -4415,7 +4424,8 @@ class CoolingOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'name': 'Cooling Outlet X',
             'name': 'Cooling Outlet X',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'cooling_port': coolingports[1].pk,
             'cooling_port': coolingports[1].pk,
             'description': 'A cooling outlet',
             'description': 'A cooling outlet',
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
@@ -4426,7 +4436,8 @@ class CoolingOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'name': 'Cooling Outlet [4-6]',
             'name': 'Cooling Outlet [4-6]',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'cooling_port': coolingports[1].pk,
             'cooling_port': coolingports[1].pk,
             'description': 'A cooling outlet',
             'description': 'A cooling outlet',
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],

+ 2 - 2
netbox/dcim/ui/panels.py

@@ -250,7 +250,7 @@ class CoolingPortPanel(panels.ObjectAttributesPanel):
     label = attrs.TextAttr('label')
     label = attrs.TextAttr('label')
     type = attrs.ChoiceAttr('type')
     type = attrs.ChoiceAttr('type')
     connector_type = attrs.ChoiceAttr('connector_type')
     connector_type = attrs.ChoiceAttr('connector_type')
-    diameter = attrs.ChoiceAttr('diameter')
+    diameter = attrs.NumericAttr('diameter', unit_accessor='get_diameter_unit_display')
     description = attrs.TextAttr('description')
     description = attrs.TextAttr('description')
     maximum_flow = attrs.TextAttr('maximum_flow', format_string=_('{} L/min'))
     maximum_flow = attrs.TextAttr('maximum_flow', format_string=_('{} L/min'))
     heat_capacity = attrs.TextAttr('heat_capacity', format_string=_('{} kW'))
     heat_capacity = attrs.TextAttr('heat_capacity', format_string=_('{} kW'))
@@ -263,7 +263,7 @@ class CoolingOutletPanel(panels.ObjectAttributesPanel):
     label = attrs.TextAttr('label')
     label = attrs.TextAttr('label')
     type = attrs.ChoiceAttr('type')
     type = attrs.ChoiceAttr('type')
     connector_type = attrs.ChoiceAttr('connector_type')
     connector_type = attrs.ChoiceAttr('connector_type')
-    diameter = attrs.ChoiceAttr('diameter')
+    diameter = attrs.NumericAttr('diameter', unit_accessor='get_diameter_unit_display')
     description = attrs.TextAttr('description')
     description = attrs.TextAttr('description')
     color = attrs.ColorAttr('color')
     color = attrs.ColorAttr('color')
     cooling_port = attrs.RelatedObjectAttr('cooling_port', linkify=True)
     cooling_port = attrs.RelatedObjectAttr('cooling_port', linkify=True)

+ 17 - 0
netbox/netbox/choices.py

@@ -7,6 +7,7 @@ __all__ = (
     'ButtonColorChoices',
     'ButtonColorChoices',
     'CSVDelimiterChoices',
     'CSVDelimiterChoices',
     'ColorChoices',
     'ColorChoices',
+    'DiameterUnitChoices',
     'DistanceUnitChoices',
     'DistanceUnitChoices',
     'ImportFormatChoices',
     'ImportFormatChoices',
     'ImportMethodChoices',
     'ImportMethodChoices',
@@ -197,3 +198,19 @@ class WeightUnitChoices(ChoiceSet):
         (UNIT_POUND, _('Pounds')),
         (UNIT_POUND, _('Pounds')),
         (UNIT_OUNCE, _('Ounces')),
         (UNIT_OUNCE, _('Ounces')),
     )
     )
+
+
+class DiameterUnitChoices(ChoiceSet):
+
+    # Metric
+    UNIT_MILLIMETER = 'mm'
+    UNIT_CENTIMETER = 'cm'
+
+    # Imperial
+    UNIT_INCH = 'in'
+
+    CHOICES = (
+        (UNIT_MILLIMETER, _('Millimeters')),
+        (UNIT_CENTIMETER, _('Centimeters')),
+        (UNIT_INCH, _('Inches')),
+    )

+ 54 - 1
netbox/netbox/models/mixins.py

@@ -3,9 +3,10 @@ from django.db import models
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
 from netbox.choices import *
 from netbox.choices import *
-from utilities.conversion import to_grams, to_meters
+from utilities.conversion import to_grams, to_meters, to_millimeters
 
 
 __all__ = (
 __all__ = (
+    'DiameterMixin',
     'DistanceMixin',
     'DistanceMixin',
     'OwnerMixin',
     'OwnerMixin',
     'WeightMixin',
     'WeightMixin',
@@ -75,6 +76,58 @@ class WeightMixin(models.Model):
             raise ValidationError(_("Must specify a unit when setting a weight"))
             raise ValidationError(_("Must specify a unit when setting a weight"))
 
 
 
 
+class DiameterMixin(models.Model):
+    diameter = models.DecimalField(
+        verbose_name=_('diameter'),
+        max_digits=8,
+        decimal_places=2,
+        blank=True,
+        null=True,
+    )
+    diameter_unit = models.CharField(
+        verbose_name=_('diameter unit'),
+        max_length=50,
+        choices=DiameterUnitChoices,
+        blank=True,
+        null=True,
+    )
+    # Stores the normalized diameter (in millimeters) for database ordering
+    _abs_diameter = models.DecimalField(
+        max_digits=13,
+        decimal_places=4,
+        blank=True,
+        null=True
+    )
+
+    class Meta:
+        abstract = True
+
+    @property
+    def abs_diameter(self):
+        # Public alias for _abs_diameter; Django templates cannot access underscore-prefixed attributes.
+        return self._abs_diameter
+
+    def save(self, *args, **kwargs):
+        # Store the given diameter (if any) in millimeters for use in database ordering
+        if self.diameter is not None and self.diameter_unit:
+            self._abs_diameter = to_millimeters(self.diameter, self.diameter_unit)
+        else:
+            self._abs_diameter = None
+
+        # Clear diameter_unit if no diameter is defined
+        if self.diameter is None:
+            self.diameter_unit = None
+
+        super().save(*args, **kwargs)
+
+    def clean(self):
+        super().clean()
+
+        # Validate diameter and diameter_unit
+        if self.diameter and not self.diameter_unit:
+            raise ValidationError(_("Must specify a unit when setting a diameter"))
+
+
 class DistanceMixin(models.Model):
 class DistanceMixin(models.Model):
     distance = models.DecimalField(
     distance = models.DecimalField(
         verbose_name=_('distance'),
         verbose_name=_('distance'),

+ 27 - 1
netbox/utilities/conversion.py

@@ -3,11 +3,12 @@ from decimal import Decimal, InvalidOperation
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 
 
 from dcim.choices import CableLengthUnitChoices
 from dcim.choices import CableLengthUnitChoices
-from netbox.choices import WeightUnitChoices
+from netbox.choices import DiameterUnitChoices, WeightUnitChoices
 
 
 __all__ = (
 __all__ = (
     'to_grams',
     'to_grams',
     'to_meters',
     'to_meters',
+    'to_millimeters',
 )
 )
 
 
 
 
@@ -66,3 +67,28 @@ def to_meters(length, unit) -> Decimal:
             valid_units=', '.join(CableLengthUnitChoices.values())
             valid_units=', '.join(CableLengthUnitChoices.values())
         )
         )
     )
     )
+
+
+def to_millimeters(diameter, unit) -> Decimal:
+    """
+    Convert the given diameter to millimeters, returning a Decimal value.
+    """
+    try:
+        diameter = Decimal(diameter)
+    except InvalidOperation:
+        raise TypeError(_("Invalid value '{diameter}' for diameter (must be a number)").format(diameter=diameter))
+    if diameter < 0:
+        raise ValueError(_("Diameter must be a positive number"))
+
+    if unit == DiameterUnitChoices.UNIT_MILLIMETER:
+        return round(Decimal(diameter), 4)
+    if unit == DiameterUnitChoices.UNIT_CENTIMETER:
+        return round(Decimal(diameter * 10), 4)
+    if unit == DiameterUnitChoices.UNIT_INCH:
+        return round(diameter * Decimal(25.4), 4)
+    raise ValueError(
+        _("Unknown unit {unit}. Must be one of the following: {valid_units}").format(
+            unit=unit,
+            valid_units=', '.join(DiameterUnitChoices.values())
+        )
+    )