diff --git a/api-client/src/calibration/types.ts b/api-client/src/calibration/types.ts
index 645f904d45b..c14ce57e64a 100644
--- a/api-client/src/calibration/types.ts
+++ b/api-client/src/calibration/types.ts
@@ -8,7 +8,7 @@ export interface PipOffsetDeletionParams {
 
 export interface TipLengthDeletionParams {
   calType: 'tipLength'
-  tiprack_hash: string
+  tiprack_uri: string
   pipette_id: string
 }
 export type DeleteCalRequestParams =
@@ -93,7 +93,7 @@ export interface TipLengthCalibration {
   source: CalibrationSourceType
   status: IndividualCalibrationHealthStatus
   id: string
-  uri?: string | null
+  uri: string
 }
 
 export interface AllTipLengthCalibrations {
diff --git a/api/src/opentrons/calibration_storage/ot2/models/v1.py b/api/src/opentrons/calibration_storage/ot2/models/v1.py
index 98f7dadca1c..585700c84c5 100644
--- a/api/src/opentrons/calibration_storage/ot2/models/v1.py
+++ b/api/src/opentrons/calibration_storage/ot2/models/v1.py
@@ -32,8 +32,12 @@ class TipLengthModel(BaseModel):
         default_factory=CalibrationStatus,
         description="The status of the calibration data.",
     )
-    uri: typing.Union[LabwareUri, Literal[""]] = Field(
-        ..., description="The tiprack URI associated with the tip length data."
+    # Old data may have a `uri` field, replaced later by `definitionHash`.
+    # uri: typing.Union[LabwareUri, Literal[""]] = Field(
+    #    ..., description="The tiprack URI associated with the tip length data."
+    # )
+    definitionHash: str = Field(
+        ..., description="The tiprack hash associated with the tip length data."
     )
 
     @validator("tipLength")
diff --git a/api/src/opentrons/calibration_storage/ot2/tip_length.py b/api/src/opentrons/calibration_storage/ot2/tip_length.py
index eca8f723f09..7aff6ec9515 100644
--- a/api/src/opentrons/calibration_storage/ot2/tip_length.py
+++ b/api/src/opentrons/calibration_storage/ot2/tip_length.py
@@ -7,6 +7,7 @@
 from opentrons import config
 
 from .. import file_operators as io, helpers, types as local_types
+from opentrons_shared_data.pipette.dev_types import LabwareUri
 
 from opentrons.protocols.api_support.constants import OPENTRONS_NAMESPACE
 from opentrons.util.helpers import utc_now
@@ -22,9 +23,9 @@
 # Get Tip Length Calibration
 
 
-def _conver_tip_length_model_to_dict(
-    to_dict: typing.Dict[str, v1.TipLengthModel]
-) -> typing.Dict[str, typing.Any]:
+def _convert_tip_length_model_to_dict(
+    to_dict: typing.Dict[LabwareUri, v1.TipLengthModel]
+) -> typing.Dict[LabwareUri, typing.Any]:
     # This is a workaround since pydantic doesn't have a nice way to
     # add encoders when converting to a dict.
     dict_of_tip_lengths = {}
@@ -35,17 +36,23 @@ def _conver_tip_length_model_to_dict(
 
 def tip_lengths_for_pipette(
     pipette_id: str,
-) -> typing.Dict[str, v1.TipLengthModel]:
+) -> typing.Dict[LabwareUri, v1.TipLengthModel]:
     tip_lengths = {}
     try:
         tip_length_filepath = config.get_tip_length_cal_path() / f"{pipette_id}.json"
         all_tip_lengths_for_pipette = io.read_cal_file(tip_length_filepath)
-        for tiprack, data in all_tip_lengths_for_pipette.items():
+        for tiprack_identifier, data in all_tip_lengths_for_pipette.items():
+            # We normally key these calibrations by their tip rack URI,
+            # but older software had them keyed by their tip rack hash.
+            # Migrate from the old format, if necessary.
+            if "/" not in tiprack_identifier:
+                data["definitionHash"] = tiprack_identifier
+                tiprack_identifier = data.pop("uri")
             try:
-                tip_lengths[tiprack] = v1.TipLengthModel(**data)
+                tip_lengths[LabwareUri(tiprack_identifier)] = v1.TipLengthModel(**data)
             except (json.JSONDecodeError, ValidationError):
                 log.warning(
-                    f"Tip length calibration is malformed for {tiprack} on {pipette_id}"
+                    f"Tip length calibration is malformed for {tiprack_identifier} on {pipette_id}"
                 )
                 pass
         return tip_lengths
@@ -64,10 +71,10 @@ def load_tip_length_calibration(
     :param pip_id: pipette you are using
     :param definition: full definition of the tiprack
     """
-    labware_hash = helpers.hash_labware_def(definition)
+    labware_uri = helpers.uri_from_definition(definition)
     load_name = definition["parameters"]["loadName"]
     try:
-        return tip_lengths_for_pipette(pip_id)[labware_hash]
+        return tip_lengths_for_pipette(pip_id)[labware_uri]
     except KeyError as e:
         raise local_types.TipLengthCalNotFound(
             f"Tip length of {load_name} has not been "
@@ -89,16 +96,16 @@ def get_all_tip_length_calibrations() -> typing.List[v1.TipLengthCalibration]:
         if filepath.stem == "index":
             continue
         tip_lengths = tip_lengths_for_pipette(filepath.stem)
-        for tiprack_hash, tip_length in tip_lengths.items():
+        for tiprack_uri, tip_length in tip_lengths.items():
             all_tip_lengths_available.append(
                 v1.TipLengthCalibration(
                     pipette=filepath.stem,
-                    tiprack=tiprack_hash,
+                    tiprack=tip_length.definitionHash,
                     tipLength=tip_length.tipLength,
                     lastModified=tip_length.lastModified,
                     source=tip_length.source,
                     status=tip_length.status,
-                    uri=tip_length.uri,
+                    uri=tiprack_uri,
                 )
             )
     return all_tip_lengths_available
@@ -129,28 +136,45 @@ def get_custom_tiprack_definition_for_tlc(labware_uri: str) -> "LabwareDefinitio
 # Delete Tip Length Calibration
 
 
-def delete_tip_length_calibration(tiprack: str, pipette_id: str) -> None:
+def delete_tip_length_calibration(
+    pipette_id: str,
+    tiprack_uri: typing.Optional[LabwareUri] = None,
+    tiprack_hash: typing.Optional[str] = None,
+) -> None:
     """
-    Delete tip length calibration based on tiprack hash and
-    pipette serial number
+    Delete tip length calibration based on an optional tiprack uri or
+    tiprack hash and pipette serial number.
 
-    :param tiprack: tiprack hash
+    :param tiprack_uri: tiprack uri
+    :param tiprack_hash: tiprack uri
     :param pipette: pipette serial number
     """
     tip_lengths = tip_lengths_for_pipette(pipette_id)
-
-    if tiprack in tip_lengths:
+    tip_length_dir = config.get_tip_length_cal_path()
+    if tiprack_uri in tip_lengths:
         # maybe make modify and delete same file?
-        del tip_lengths[tiprack]
-        tip_length_dir = config.get_tip_length_cal_path()
+        del tip_lengths[tiprack_uri]
+
+        if tip_lengths:
+            dict_of_tip_lengths = _convert_tip_length_model_to_dict(tip_lengths)
+            io.save_to_file(tip_length_dir, pipette_id, dict_of_tip_lengths)
+        else:
+            io.delete_file(tip_length_dir / f"{pipette_id}.json")
+    elif tiprack_hash and any(tiprack_hash in v.dict() for v in tip_lengths.values()):
+        # NOTE this is for backwards compatibilty only
+        # TODO delete this check once the tip_length DELETE router
+        # no longer depends on a tiprack hash
+        for k, v in tip_lengths.items():
+            if tiprack_hash in v.dict():
+                tip_lengths.pop(k)
         if tip_lengths:
-            dict_of_tip_lengths = _conver_tip_length_model_to_dict(tip_lengths)
+            dict_of_tip_lengths = _convert_tip_length_model_to_dict(tip_lengths)
             io.save_to_file(tip_length_dir, pipette_id, dict_of_tip_lengths)
         else:
             io.delete_file(tip_length_dir / f"{pipette_id}.json")
     else:
         raise local_types.TipLengthCalNotFound(
-            f"Tip length for hash {tiprack} has not been "
+            f"Tip length for uri {tiprack_uri} and hash {tiprack_hash} has not been "
             f"calibrated for this pipette: {pipette_id} and cannot"
             "be loaded"
         )
@@ -176,7 +200,7 @@ def create_tip_length_data(
     cal_status: typing.Optional[
         typing.Union[local_types.CalibrationStatus, v1.CalibrationStatus]
     ] = None,
-) -> typing.Dict[str, v1.TipLengthModel]:
+) -> typing.Dict[LabwareUri, v1.TipLengthModel]:
     """
     Function to correctly format tip length data.
 
@@ -197,13 +221,13 @@ def create_tip_length_data(
         lastModified=utc_now(),
         source=local_types.SourceType.user,
         status=cal_status_model,
-        uri=labware_uri,
+        definitionHash=labware_hash,
     )
 
     if not definition.get("namespace") == OPENTRONS_NAMESPACE:
         _save_custom_tiprack_definition(labware_uri, definition)
 
-    data = {labware_hash: tip_length_data}
+    data = {labware_uri: tip_length_data}
     return data
 
 
@@ -220,7 +244,7 @@ def _save_custom_tiprack_definition(
 
 def save_tip_length_calibration(
     pip_id: str,
-    tip_length_cal: typing.Dict[str, v1.TipLengthModel],
+    tip_length_cal: typing.Dict[LabwareUri, v1.TipLengthModel],
 ) -> None:
     """
     Function used to save tip length calibration to file.
@@ -235,5 +259,5 @@ def save_tip_length_calibration(
 
     all_tip_lengths.update(tip_length_cal)
 
-    dict_of_tip_lengths = _conver_tip_length_model_to_dict(all_tip_lengths)
+    dict_of_tip_lengths = _convert_tip_length_model_to_dict(all_tip_lengths)
     io.save_to_file(tip_length_dir_path, pip_id, dict_of_tip_lengths)
diff --git a/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py b/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py
index 29900d68a6d..f2f8a7fc426 100644
--- a/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py
+++ b/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py
@@ -130,18 +130,15 @@ def load_tip_length_for_pipette(
         pipette_id, tiprack
     )
 
-    # TODO (lc 09-26-2022) We shouldn't have to do a hash twice. We should figure out what
-    # information we actually need from the labware definition and pass it into
-    # the `load_tip_length_calibration` function.
-    tiprack_hash = helpers.hash_labware_def(tiprack)
+    tiprack_uri = helpers.uri_from_definition(tiprack)
 
     return TipLengthCalibration(
         tip_length=tip_length_data.tipLength,
         source=tip_length_data.source,
         pipette=pipette_id,
-        tiprack=tiprack_hash,
+        tiprack=tip_length_data.definitionHash,
         last_modified=tip_length_data.lastModified,
-        uri=tip_length_data.uri,
+        uri=tiprack_uri,
         status=types.CalibrationStatus(
             markedAt=tip_length_data.status.markedAt,
             markedBad=tip_length_data.status.markedBad,
diff --git a/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py b/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py
index 93a208e0071..4b63b52d3fc 100644
--- a/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py
+++ b/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py
@@ -1,9 +1,11 @@
 import pytest
-from typing import cast, Any, TYPE_CHECKING
+from typing import Any, TYPE_CHECKING
 
+from opentrons import config
 from opentrons.calibration_storage import (
     types as cs_types,
     helpers,
+    file_operators as io,
 )
 
 from opentrons.calibration_storage.ot2 import (
@@ -15,10 +17,10 @@
     clear_tip_length_calibration,
     models,
 )
+from opentrons_shared_data.pipette.dev_types import LabwareUri
 
 if TYPE_CHECKING:
     from opentrons_shared_data.labware.dev_types import LabwareDefinition
-    from opentrons_shared_data.pipette.dev_types import LabwareUri
 
 
 @pytest.fixture
@@ -38,6 +40,18 @@ def starting_calibration_data(
     save_tip_length_calibration("pip1", tip_length1)
     save_tip_length_calibration("pip2", tip_length2)
     save_tip_length_calibration("pip1", tip_length3)
+    inside_data = tip_length3[LabwareUri("dummy_namespace/minimal_labware_def/1")]
+    data = {
+        inside_data.definitionHash: {
+            "tipLength": 27,
+            "lastModified": inside_data.lastModified.isoformat(),
+            "source": inside_data.source,
+            "status": inside_data.status.dict(),
+            "uri": "dummy_namespace/minimal_labware_def/1",
+        }
+    }
+    tip_length_dir_path = config.get_tip_length_cal_path()
+    io.save_to_file(tip_length_dir_path, "pip2", data)
 
 
 def test_save_tip_length_calibration(
@@ -48,13 +62,13 @@ def test_save_tip_length_calibration(
     """
     assert tip_lengths_for_pipette("pip1") == {}
     assert tip_lengths_for_pipette("pip2") == {}
-    tip_rack_hash = helpers.hash_labware_def(minimal_labware_def)
+    tip_rack_uri = helpers.uri_from_definition(minimal_labware_def)
     tip_length1 = create_tip_length_data(minimal_labware_def, 22.0)
     tip_length2 = create_tip_length_data(minimal_labware_def, 31.0)
     save_tip_length_calibration("pip1", tip_length1)
     save_tip_length_calibration("pip2", tip_length2)
-    assert tip_lengths_for_pipette("pip1")[tip_rack_hash].tipLength == 22.0
-    assert tip_lengths_for_pipette("pip2")[tip_rack_hash].tipLength == 31.0
+    assert tip_lengths_for_pipette("pip1")[tip_rack_uri].tipLength == 22.0
+    assert tip_lengths_for_pipette("pip2")[tip_rack_uri].tipLength == 31.0
 
 
 def test_get_tip_length_calibration(
@@ -64,11 +78,12 @@ def test_get_tip_length_calibration(
     Test ability to get a tip length calibration model.
     """
     tip_length_data = load_tip_length_calibration("pip1", minimal_labware_def)
+    tip_rack_hash = helpers.hash_labware_def(minimal_labware_def)
     assert tip_length_data == models.v1.TipLengthModel(
         tipLength=22.0,
         source=cs_types.SourceType.user,
         lastModified=tip_length_data.lastModified,
-        uri=cast("LabwareUri", "opentronstest/minimal_labware_def/1"),
+        definitionHash=tip_rack_hash,
     )
 
     with pytest.raises(cs_types.TipLengthCalNotFound):
@@ -83,8 +98,8 @@ def test_delete_specific_tip_calibration(
     """
     assert len(tip_lengths_for_pipette("pip1").keys()) == 2
     assert tip_lengths_for_pipette("pip2") != {}
-    tip_rack_hash = helpers.hash_labware_def(minimal_labware_def)
-    delete_tip_length_calibration(tip_rack_hash, "pip1")
+    tip_rack_uri = helpers.uri_from_definition(minimal_labware_def)
+    delete_tip_length_calibration("pip1", tiprack_uri=tip_rack_uri)
     assert len(tip_lengths_for_pipette("pip1").keys()) == 1
     assert tip_lengths_for_pipette("pip2") != {}
 
diff --git a/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py b/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py
index b850803ba61..6aa3ca2a009 100644
--- a/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py
+++ b/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py
@@ -81,7 +81,7 @@ def test_load_tip_length(
     tip_length_data = v1_models.TipLengthModel(
         tipLength=1.23,
         lastModified=datetime(year=2023, month=1, day=1),
-        uri=LabwareUri("def456"),
+        definitionHash="asdfghjk",
         source=subject.SourceType.factory,
         status=v1_models.CalibrationStatus(
             markedBad=True,
@@ -99,6 +99,9 @@ def test_load_tip_length(
     decoy.when(calibration_storage.helpers.hash_labware_def(tip_rack_dict)).then_return(
         "asdfghjk"
     )
+    decoy.when(
+        calibration_storage.helpers.uri_from_definition(tip_rack_dict)
+    ).then_return(LabwareUri("def456"))
 
     result = subject.load_tip_length_for_pipette(
         pipette_id="abc123", tiprack=tip_rack_definition
diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json
index 8d959836e18..5dd0f2c0346 100644
--- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json
+++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json
@@ -564,7 +564,7 @@
           "errorInfo": {
             "args": "('nest_1_reservoir_290ml in slot C4 prevents temperatureModuleV2 from using slot C3.',)",
             "class": "DeckConflictError",
-            "traceback": "  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 69, in run_python\n    exec(\"run(__context)\", new_globs)\n\n  File \"<string>\", line 1, in <module>\n\n  File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3.py\", line 17, in run\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n    return decorated_obj(*args, **kwargs)\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 818, in load_module\n    module_core = self._core.load_module(\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 435, in load_module\n    deck_conflict.check(\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 185, in check\n    wrapped_deck_conflict.check(\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 224, in check\n    raise DeckConflictError(\n"
+            "traceback": "  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 69, in run_python\n    exec(\"run(__context)\", new_globs)\n\n  File \"<string>\", line 1, in <module>\n\n  File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3.py\", line 17, in run\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n    return decorated_obj(*args, **kwargs)\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 818, in load_module\n    module_core = self._core.load_module(\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 435, in load_module\n    deck_conflict.check(\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 190, in check\n    wrapped_deck_conflict.check(\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 224, in check\n    raise DeckConflictError(\n"
           },
           "errorType": "PythonException",
           "wrappedErrors": []
diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a185c4e1c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_Smoke].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a185c4e1c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_Smoke].json
index c638710d9ea..f363e79201f 100644
--- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a185c4e1c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_Smoke].json
+++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a185c4e1c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_Smoke].json
@@ -8271,6 +8271,7 @@
         "addressableAreaName": "movableTrashB3",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -8373,6 +8374,7 @@
         "addressableAreaName": "movableTrashB3",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -8475,6 +8477,7 @@
         "addressableAreaName": "movableTrashB3",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -8577,6 +8580,7 @@
         "addressableAreaName": "movableTrashB3",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -8679,6 +8683,7 @@
         "addressableAreaName": "movableTrashB3",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -8781,6 +8786,7 @@
         "addressableAreaName": "movableTrashB3",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -8883,6 +8889,7 @@
         "addressableAreaName": "movableTrashB3",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -8985,6 +8992,7 @@
         "addressableAreaName": "movableTrashB3",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -9087,6 +9095,7 @@
         "addressableAreaName": "movableTrashB3",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -9189,6 +9198,7 @@
         "addressableAreaName": "movableTrashB3",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -9291,6 +9301,7 @@
         "addressableAreaName": "movableTrashB3",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -9393,6 +9404,7 @@
         "addressableAreaName": "movableTrashB3",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -9641,6 +9653,7 @@
         "addressableAreaName": "movableTrashB3",
         "alternateDropLocation": false,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -10316,6 +10329,7 @@
         "addressableAreaName": "movableTrashB3",
         "alternateDropLocation": false,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a32a763f5][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1_NoModules].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a32a763f5][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1_NoModules].json
index 29699064add..e1749edf244 100644
--- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a32a763f5][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1_NoModules].json
+++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a32a763f5][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1_NoModules].json
@@ -10514,6 +10514,7 @@
         "addressableAreaName": "movableTrashC1",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -10758,6 +10759,7 @@
         "addressableAreaName": "movableTrashD1",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json
index 2a4ecdd58d3..ac2117946a3 100644
--- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json
+++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json
@@ -6949,7 +6949,7 @@
           "errorInfo": {
             "args": "('Cannot aspirate more than pipette max volume',)",
             "class": "AssertionError",
-            "traceback": "  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 90, in _run\n    await self._run_func()\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 173, in execute\n    await to_thread.run_sync(run_protocol, protocol, context)\n\n  File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 31, in run_sync\n    return await get_asynclib().run_sync_in_worker_thread(\n\n  File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 937, in run_sync_in_worker_thread\n    return await future\n\n  File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 867, in run\n    result = context.run(func, *args)\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 45, in run_protocol\n    execute_json_v4.dispatch_json(\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v4.py\", line 272, in dispatch_json\n    pipette_command_map[command_type](  # type: ignore\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v3.py\", line 159, in _aspirate\n    pipette.aspirate(volume, location)\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n    return decorated_obj(*args, **kwargs)\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 267, in aspirate\n    self._core.aspirate(\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n    new_volume <= self._pipette_dict[\"working_volume\"]\n"
+            "traceback": "  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 90, in _run\n    await self._run_func()\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 173, in execute\n    await to_thread.run_sync(run_protocol, protocol, context)\n\n  File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n    return await get_asynclib().run_sync_in_worker_thread(\n\n  File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n    return await future\n\n  File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n    result = context.run(func, *args)\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 45, in run_protocol\n    execute_json_v4.dispatch_json(\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v4.py\", line 272, in dispatch_json\n    pipette_command_map[command_type](  # type: ignore\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v3.py\", line 159, in _aspirate\n    pipette.aspirate(volume, location)\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n    return decorated_obj(*args, **kwargs)\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 267, in aspirate\n    self._core.aspirate(\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n    new_volume <= self._pipette_dict[\"working_volume\"]\n"
           },
           "errorType": "PythonException",
           "wrappedErrors": []
diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5eb46a4f85][Flex_P1000_96_GRIPPER_2_16_AnalysisError_DropLabwareIntoTrashBin].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5eb46a4f85][Flex_P1000_96_GRIPPER_2_16_AnalysisError_DropLabwareIntoTrashBin].json
index 93b099eae94..af05109c1e0 100644
--- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5eb46a4f85][Flex_P1000_96_GRIPPER_2_16_AnalysisError_DropLabwareIntoTrashBin].json
+++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5eb46a4f85][Flex_P1000_96_GRIPPER_2_16_AnalysisError_DropLabwareIntoTrashBin].json
@@ -1257,6 +1257,7 @@
         "addressableAreaName": "movableTrashC3",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json
index 872147c26bc..c2eba70dccc 100644
--- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json
+++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json
@@ -30,7 +30,7 @@
             "msg": "No module named 'superspecialmagic'",
             "name": "superspecialmagic",
             "path": "None",
-            "traceback": "  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 90, in _run\n    await self._run_func()\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 173, in execute\n    await to_thread.run_sync(run_protocol, protocol, context)\n\n  File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 31, in run_sync\n    return await get_asynclib().run_sync_in_worker_thread(\n\n  File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 937, in run_sync_in_worker_thread\n    return await future\n\n  File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 867, in run\n    result = context.run(func, *args)\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 27, in run_protocol\n    run_python(protocol, context)\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 46, in run_python\n    exec(proto.contents, new_globs)\n\n  File \"OT2_None_None_2_13_PythonSyntaxError.py\", line 4, in <module>\n"
+            "traceback": "  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 90, in _run\n    await self._run_func()\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 173, in execute\n    await to_thread.run_sync(run_protocol, protocol, context)\n\n  File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n    return await get_asynclib().run_sync_in_worker_thread(\n\n  File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n    return await future\n\n  File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n    result = context.run(func, *args)\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 27, in run_protocol\n    run_python(protocol, context)\n\n  File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 46, in run_python\n    exec(proto.contents, new_globs)\n\n  File \"OT2_None_None_2_13_PythonSyntaxError.py\", line 4, in <module>\n"
           },
           "errorType": "PythonException",
           "wrappedErrors": []
diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a23a1de3ce][OT2_P300M_P20S_TC_HS_TM_2_16_SmokeTestV3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a23a1de3ce][OT2_P300M_P20S_TC_HS_TM_2_16_SmokeTestV3].json
index 49b64623b97..0c7c361123c 100644
--- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a23a1de3ce][OT2_P300M_P20S_TC_HS_TM_2_16_SmokeTestV3].json
+++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a23a1de3ce][OT2_P300M_P20S_TC_HS_TM_2_16_SmokeTestV3].json
@@ -11102,6 +11102,7 @@
         "addressableAreaName": "fixedTrash",
         "alternateDropLocation": false,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -11137,6 +11138,7 @@
         "addressableAreaName": "fixedTrash",
         "alternateDropLocation": false,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -11216,6 +11218,7 @@
         "addressableAreaName": "fixedTrash",
         "alternateDropLocation": false,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -11295,6 +11298,7 @@
         "addressableAreaName": "fixedTrash",
         "alternateDropLocation": false,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -11374,6 +11378,7 @@
         "addressableAreaName": "fixedTrash",
         "alternateDropLocation": false,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -11453,6 +11458,7 @@
         "addressableAreaName": "fixedTrash",
         "alternateDropLocation": false,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -11532,6 +11538,7 @@
         "addressableAreaName": "fixedTrash",
         "alternateDropLocation": false,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -11561,6 +11568,7 @@
         "addressableAreaName": "fixedTrash",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -14985,6 +14993,7 @@
         "addressableAreaName": "fixedTrash",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -15186,6 +15195,7 @@
         "addressableAreaName": "fixedTrash",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -15263,6 +15273,7 @@
         "addressableAreaName": "fixedTrash",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afe15b729c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afe15b729c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1].json
index caffbd78786..1bb7131a414 100644
--- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afe15b729c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1].json
+++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afe15b729c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1].json
@@ -12283,6 +12283,7 @@
         "addressableAreaName": "movableTrashC1",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
@@ -12527,6 +12528,7 @@
         "addressableAreaName": "movableTrashD1",
         "alternateDropLocation": true,
         "forceDirect": false,
+        "ignoreTipConfiguration": true,
         "offset": {
           "x": 0.0,
           "y": 0.0,
diff --git a/app/src/organisms/Devices/hooks/__fixtures__/taskListFixtures.ts b/app/src/organisms/Devices/hooks/__fixtures__/taskListFixtures.ts
index e23593fe599..e7613939722 100644
--- a/app/src/organisms/Devices/hooks/__fixtures__/taskListFixtures.ts
+++ b/app/src/organisms/Devices/hooks/__fixtures__/taskListFixtures.ts
@@ -91,6 +91,7 @@ export const mockBadTipLengthCalibrations: TipLengthCalibration[] = [
     source: 'user',
     status: { markedBad: true, source: null, markedAt: null },
     id: 'test-tip-length-id-1',
+    uri: 'test-uri',
   },
   {
     tipLength: 0,
@@ -100,6 +101,7 @@ export const mockBadTipLengthCalibrations: TipLengthCalibration[] = [
     source: 'user',
     status: { markedBad: true, source: null, markedAt: null },
     id: 'test-tip-length-id-2',
+    uri: 'test-uri-2',
   },
 ]
 
@@ -112,6 +114,7 @@ export const mockCompleteTipLengthCalibrations: TipLengthCalibration[] = [
     source: 'user',
     status: { markedBad: false, source: null, markedAt: null },
     id: 'test-tip-length-id-1',
+    uri: 'test-uri',
   },
   {
     tipLength: 0,
@@ -121,6 +124,7 @@ export const mockCompleteTipLengthCalibrations: TipLengthCalibration[] = [
     source: 'user',
     status: { markedBad: false, source: null, markedAt: null },
     id: 'test-tip-length-id-2',
+    uri: 'test-uri-2',
   },
 ]
 
@@ -133,6 +137,7 @@ export const mockIncompleteTipLengthCalibrations: TipLengthCalibration[] = [
     source: 'user',
     status: { markedBad: false, source: null, markedAt: null },
     id: 'test-tip-length-id-2',
+    uri: 'test-uri-2',
   },
 ]
 
diff --git a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx
index 228d17155d1..aa6a1a68536 100644
--- a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx
+++ b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx
@@ -160,7 +160,7 @@ export function OverflowMenu({
       if (applicableTipLengthCal == null) return
       params = {
         calType,
-        tiprack_hash: applicableTipLengthCal.tiprack,
+        tiprack_uri: applicableTipLengthCal.uri,
         pipette_id: applicableTipLengthCal.pipette,
       }
     }
diff --git a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx
index a1341620419..b518dcb6ce1 100644
--- a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx
+++ b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx
@@ -261,7 +261,7 @@ describe('OverflowMenu', () => {
     }
     const expectedCallParams = {
       calType: 'tipLength',
-      tiprack_hash: mockTipLengthCalibrationResponse.tiprack,
+      tiprack_uri: mockTipLengthCalibrationResponse.uri,
       pipette_id: mockTipLengthCalibrationResponse.pipette,
     }
     const [{ getByText, getByLabelText }] = render(props)
diff --git a/app/src/redux/calibration/api-types.ts b/app/src/redux/calibration/api-types.ts
index d12f752e021..624af038c00 100644
--- a/app/src/redux/calibration/api-types.ts
+++ b/app/src/redux/calibration/api-types.ts
@@ -128,7 +128,7 @@ export interface TipLengthCalibration {
   source: CalibrationSource
   status: IndividualCalibrationStatus
   id: string
-  uri?: string | null
+  uri: string
 }
 
 export interface AllTipLengthCalibrations {
diff --git a/app/src/redux/calibration/tip-length/__fixtures__/tip-length-calibration.ts b/app/src/redux/calibration/tip-length/__fixtures__/tip-length-calibration.ts
index b6820445451..86257389b3b 100644
--- a/app/src/redux/calibration/tip-length/__fixtures__/tip-length-calibration.ts
+++ b/app/src/redux/calibration/tip-length/__fixtures__/tip-length-calibration.ts
@@ -23,6 +23,7 @@ export const mockTipLengthCalibration1: TipLengthCalibration = {
     markedAt: '',
   },
   id: 'someID',
+  uri: 'test-uri',
 }
 
 export const mockTipLengthCalibration2: TipLengthCalibration = {
@@ -37,6 +38,7 @@ export const mockTipLengthCalibration2: TipLengthCalibration = {
     markedAt: '',
   },
   id: 'someID',
+  uri: 'test-uri',
 }
 
 export const mockTipLengthCalibration3: TipLengthCalibration = {
@@ -51,6 +53,7 @@ export const mockTipLengthCalibration3: TipLengthCalibration = {
     markedAt: '',
   },
   id: 'someID',
+  uri: 'test-uri',
 }
 
 export const mockPipetteMatchTipLengthCalibration: AllTipLengthCalibrations = {
diff --git a/hardware/opentrons_hardware/firmware_bindings/constants.py b/hardware/opentrons_hardware/firmware_bindings/constants.py
index 61387ea798d..6d173e6effc 100644
--- a/hardware/opentrons_hardware/firmware_bindings/constants.py
+++ b/hardware/opentrons_hardware/firmware_bindings/constants.py
@@ -250,6 +250,13 @@ class MessageId(int, Enum):
     peripheral_status_response = 0x8D
     baseline_sensor_response = 0x8E
 
+    set_hepa_fan_state_request = 0x90
+    get_hepa_fan_state_request = 0x91
+    get_hepa_fan_state_response = 0x92
+    set_hepa_uv_state_request = 0x93
+    get_hepa_uv_state_request = 0x94
+    get_hepa_uv_state_response = 0x95
+
 
 @unique
 class ErrorSeverity(int, Enum):
diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py b/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py
index 9af02770745..49698329264 100644
--- a/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py
+++ b/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py
@@ -907,3 +907,73 @@ class HepaUVInfoResponse(BaseMessage):  # noqa: D101
         payloads.HepaUVInfoResponsePayload
     ] = payloads.HepaUVInfoResponsePayload
     message_id: Literal[MessageId.hepauv_info_response] = MessageId.hepauv_info_response
+
+
+@dataclass
+class SetHepaFanStateRequest(BaseMessage):
+    """Request to set the state and duty cycle of the hepa fan."""
+
+    payload: payloads.SetHepaFanStateRequestPayload
+    payload_type: Type[
+        payloads.SetHepaFanStateRequestPayload
+    ] = payloads.SetHepaFanStateRequestPayload
+    message_id: Literal[
+        MessageId.set_hepa_fan_state_request
+    ] = MessageId.set_hepa_fan_state_request
+
+
+@dataclass
+class GetHepaFanStateRequest(EmptyPayloadMessage):
+    """Request the Hepa/UV to send the state and duty cycle of the fan."""
+
+    message_id: Literal[
+        MessageId.get_hepa_fan_state_request
+    ] = MessageId.get_hepa_fan_state_request
+
+
+@dataclass
+class GetHepaFanStateResponse(BaseMessage):
+    """Hepa/UV response with the state and duty cycle of the fan."""
+
+    payload: payloads.GetHepaFanStatePayloadResponse
+    payload_type: Type[
+        payloads.GetHepaFanStatePayloadResponse
+    ] = payloads.GetHepaFanStatePayloadResponse
+    message_id: Literal[
+        MessageId.get_hepa_fan_state_response
+    ] = MessageId.get_hepa_fan_state_response
+
+
+@dataclass
+class SetHepaUVStateRequest(BaseMessage):
+    """Sets the state and timeout in seconds the UV light should stay on."""
+
+    payload: payloads.SetHepaUVStateRequestPayload
+    payload_type: Type[
+        payloads.SetHepaUVStateRequestPayload
+    ] = payloads.SetHepaUVStateRequestPayload
+    message_id: Literal[
+        MessageId.set_hepa_uv_state_request
+    ] = MessageId.set_hepa_uv_state_request
+
+
+@dataclass
+class GetHepaUVStateRequest(EmptyPayloadMessage):
+    """Request the Hepa/UV send the state and timeout in seconds for the UV light."""
+
+    message_id: Literal[
+        MessageId.get_hepa_uv_state_request
+    ] = MessageId.get_hepa_uv_state_request
+
+
+@dataclass
+class GetHepaUVStateResponse(BaseMessage):
+    """Response from the Hepa/UV state and timeout in seconds for the UV light."""
+
+    payload: payloads.GetHepaUVStatePayloadResponse
+    payload_type: Type[
+        payloads.GetHepaUVStatePayloadResponse
+    ] = payloads.GetHepaUVStatePayloadResponse
+    message_id: Literal[
+        MessageId.get_hepa_uv_state_response
+    ] = MessageId.get_hepa_uv_state_response
diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/messages.py b/hardware/opentrons_hardware/firmware_bindings/messages/messages.py
index b1563f5ecf4..930c82bab79 100644
--- a/hardware/opentrons_hardware/firmware_bindings/messages/messages.py
+++ b/hardware/opentrons_hardware/firmware_bindings/messages/messages.py
@@ -100,6 +100,12 @@
     defs.SetGripperJawHoldoffRequest,
     defs.GripperJawHoldoffRequest,
     defs.GripperJawHoldoffResponse,
+    defs.SetHepaFanStateRequest,
+    defs.GetHepaFanStateRequest,
+    defs.GetHepaFanStateResponse,
+    defs.SetHepaUVStateRequest,
+    defs.GetHepaUVStateRequest,
+    defs.GetHepaUVStateResponse,
 ]
 
 
diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py
index 650c5d1e30c..c2efd8ac416 100644
--- a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py
+++ b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py
@@ -631,7 +631,40 @@ def build(cls, data: bytes) -> "GetMotorUsageResponsePayload":
 
 @dataclass(eq=False)
 class HepaUVInfoResponsePayload(EmptyPayload):
-    """A response carrying data about an attached gripper."""
+    """A response carrying data about an attached hepa uv."""
 
     model: utils.UInt16Field
     serial: SerialDataCodeField
+
+
+@dataclass(eq=False)
+class SetHepaFanStateRequestPayload(EmptyPayload):
+    """A request to set the state and pwm of a the hepa fan."""
+
+    duty_cycle: utils.UInt32Field
+    fan_on: utils.Int8Field
+
+
+@dataclass(eq=False)
+class GetHepaFanStatePayloadResponse(EmptyPayload):
+    """A response with the state and pwm of the fan."""
+
+    duty_cycle: utils.UInt32Field
+    fan_on: utils.UInt8Field
+
+
+@dataclass(eq=False)
+class SetHepaUVStateRequestPayload(EmptyPayload):
+    """A request to set the state and timeout in seconds of the hepa uv light."""
+
+    timeout_s: utils.UInt32Field
+    uv_light_on: utils.UInt8Field
+
+
+@dataclass(eq=False)
+class GetHepaUVStatePayloadResponse(EmptyPayload):
+    """A response with the state and timeout in seconds of the hepa uv light."""
+
+    timeout_s: utils.UInt32Field
+    uv_light_on: utils.UInt8Field
+    remaining_time_s: utils.UInt32Field
diff --git a/robot-server/robot_server/service/tip_length/router.py b/robot-server/robot_server/service/tip_length/router.py
index 2d6461e0b7f..f1b5fa3166a 100644
--- a/robot-server/robot_server/service/tip_length/router.py
+++ b/robot-server/robot_server/service/tip_length/router.py
@@ -1,6 +1,6 @@
 from starlette import status
 from fastapi import APIRouter, Depends
-from typing import Optional
+from typing import Optional, cast
 
 from opentrons.calibration_storage import types as cal_types
 from opentrons.calibration_storage.ot2 import tip_length, models
@@ -12,6 +12,7 @@
 from robot_server.service.shared_models import calibration as cal_model
 
 from opentrons.hardware_control import API
+from opentrons_shared_data.pipette.dev_types import LabwareUri
 
 
 router = APIRouter()
@@ -80,17 +81,24 @@ async def get_all_tip_length_calibrations(
 @router.delete(
     "/calibration/tip_length",
     description="Delete one specific tip length calibration by pipette "
-    "serial and tiprack hash",
+    "serial and tiprack uri",
     responses={status.HTTP_404_NOT_FOUND: {"model": ErrorBody}},
 )
 async def delete_specific_tip_length_calibration(
-    tiprack_hash: str, pipette_id: str, _: API = Depends(get_ot2_hardware)
+    pipette_id: str,
+    tiprack_hash: Optional[str] = None,
+    tiprack_uri: Optional[str] = None,
+    _: API = Depends(get_ot2_hardware),
 ):
     try:
-        tip_length.delete_tip_length_calibration(tiprack_hash, pipette_id)
+        tip_length.delete_tip_length_calibration(
+            pipette_id,
+            tiprack_uri=cast(LabwareUri, tiprack_uri),
+            tiprack_hash=tiprack_hash,
+        )
     except cal_types.TipLengthCalNotFound:
         raise RobotServerError(
             definition=CommonErrorDef.RESOURCE_NOT_FOUND,
             resource="TipLengthCalibration",
-            id=f"{tiprack_hash}&{pipette_id}",
+            id=f"{tiprack_uri}&{pipette_id}",
         )
diff --git a/robot-server/tests/integration/test_tip_length_access.tavern.yaml b/robot-server/tests/integration/test_tip_length_access.tavern.yaml
index 9b181e0877a..35e8f6d2d07 100644
--- a/robot-server/tests/integration/test_tip_length_access.tavern.yaml
+++ b/robot-server/tests/integration/test_tip_length_access.tavern.yaml
@@ -148,14 +148,14 @@ marks: *cal_marks
 stages:
   - name: DELETE request with correct pipette AND tiprack
     request:
-      url: "{ot2_server_base_url}/calibration/tip_length?pipette_id=321&tiprack_hash=130e17bb7b2f0c0472dcc01c1ff6f600ca1a6f9f86a90982df56c4bf43776824"
+      url: "{ot2_server_base_url}/calibration/tip_length?pipette_id=321&tiprack_uri=opentrons/opentrons_96_filtertiprack_200ul/1"
       method: DELETE
     response:
         status_code: 200
 
   - name: DELETE request with incorrect pipette AND tiprack
     request:
-      url: "{ot2_server_base_url}/calibration/tip_length?pipette_id=321&tiprack_hash=wronghash"
+      url: "{ot2_server_base_url}/calibration/tip_length?pipette_id=321&tiprack_uri=wronguri"
       method: DELETE
     response:
         status_code: 404
diff --git a/robot-server/tests/service/tip_length/test_tip_length_management.py b/robot-server/tests/service/tip_length/test_tip_length_management.py
index 628e6b0df29..1103e0c2703 100644
--- a/robot-server/tests/service/tip_length/test_tip_length_management.py
+++ b/robot-server/tests/service/tip_length/test_tip_length_management.py
@@ -1,5 +1,6 @@
 PIPETTE_ID = "123"
 LW_HASH = "130e17bb7b2f0c0472dcc01c1ff6f600ca1a6f9f86a90982df56c4bf43776824"
+LW_URI = "opentrons/opentrons_96_filtertiprack_200ul/1"
 FAKE_PIPETTE_ID = "fake_pip"
 WRONG_LW_HASH = "wronghash"
 
@@ -32,12 +33,10 @@ def test_access_tip_length_calibration(api_client, set_up_tip_length_temp_direct
     assert resp.json()["data"] == []
 
 
-def test_delete_tip_length_calibration(
-    api_client, set_up_pipette_offset_temp_directory
-):
+def test_delete_tip_length_calibration(api_client, set_up_tip_length_temp_directory):
     resp = api_client.delete(
         f"/calibration/tip_length?pipette_id={FAKE_PIPETTE_ID}&"
-        f"tiprack_hash={WRONG_LW_HASH}"
+        f"tiprack_uri={WRONG_LW_HASH}"
     )
     assert resp.status_code == 404
     body = resp.json()
@@ -53,7 +52,19 @@ def test_delete_tip_length_calibration(
         ]
     }
 
+    resp = api_client.get(
+        f"/calibration/tip_length?pipette_id={PIPETTE_ID}&" f"tiprack_uri={LW_URI}"
+    )
+    assert resp.status_code == 200
+    assert resp.json()["data"][0]["uri"] == LW_URI
+
     resp = api_client.delete(
-        f"/calibration/tip_length?pipette_id={PIPETTE_ID}&" f"tiprack_hash={LW_HASH}"
+        f"/calibration/tip_length?pipette_id={PIPETTE_ID}&" f"tiprack_uri={LW_URI}"
     )
     assert resp.status_code == 200
+
+    resp = api_client.get(
+        f"/calibration/tip_length?pipette_id={PIPETTE_ID}&" f"tiprack_uri={LW_URI}"
+    )
+    assert resp.status_code == 200
+    assert resp.json()["data"] == []